Object的变化检测
1.数据驱动视图
- 什么是数据驱动视图?
- 首先可以将数据理解为状态,而视图就是用户可直观看到页面,因为页面不可能是一成不变的即它应该是动态变化的,而它的变化也不应该是无迹可寻的,它或者是由用户操作引起的,亦或者是由后端数据变化引起的,不管它是因为什么引起的,都可以统称为它的状态变了,它由前一个状态变到了后一个状态,页面也就应该随之而变化。
- 可归结为表达公式: UI = render(state)
- 上述中的状态state是输入,页面UI输出,状态输入一旦变化了,页面输出也随之而变化。即把这种特性称之为数据驱动视图。
- 在数据驱动视图公式中state和UI都是用户定的,而不变的是这个render(),所以Vue就扮演了render()这个角色,当Vue发现state变化之后,经过一系列加工,最终将变化反应在UI上。那么第一个问题来了,Vue怎么知道state变化了呢?
2.变化检测
- 什么是变化检测?
- 变化侦测就是追踪状态,亦或者说是数据的变化,一旦发生了变化,就要去更新视图。
- Vue中有自己的一套变化侦测实现机制。
2.1源码出发解读Vue的变化侦测实现机制。
-
什么是数据可观测?
- 数据的每次读和写能够被我们看的见,即我们能够知道数据什么时候被读取了或数据什么时候被改写了,将其称为数据变的‘可观测’。
-
JS为我们提供了Object.defineProperty方法,使Object数据变得“可观测”。
-
定义一个car对象如下:
let car = { 'brand':'BMW', 'price':3000 } // 改变car数据 car.brand = 'Tesla' car.price = 4000
-
上面代码中当这个car的属性被读取或修改时,我们并不知情。那么应该如何做才能够让car主动告诉我们,它的属性被修改了呢?
-
使用Object.defineProperty()改写上面的例子后如下:
let car = {} let brand = 'BMW' let price = 3000 Object.defineProperty(car, 'brand' , { enumerable: true, // 是否可枚举 configurable: true, // 是否可配置 // 读取时 get(){ console.log('brand属性被读取了') return brand }, // 修改时 set(newVal){ console.log('brand属性被修改了') brand = newVal } } ) Object.defineProperty(car, 'price', { enumerable: true, // 是否可枚举 configurable: true, // 是否可配置 // 读取时 get(){ console.log('price属性被读取了') return price }, // 修改时 set(newVal){ console.log('price属性被修改了') price = newVal } })
-
通过Object.defineProperty()方法给car空对象重新定义了brand和price属性,并把这两个属性的读和写分别使用get()和set()进行拦截,每当该属性进行读或写操作的时候就会触发get()和set()。
- 可以看到,car已经可以主动告诉我们它的属性的读写情况了,这也意味着,这个car的数据对象已经是“可观测”的了。
- 为了把其他自定义的属性都变得可观测,可以进一步编写代码,如下:
// 源码位置:src/core/observer/index.js /** * Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象 */ export class Observer { // 监听类 constructor (value) { this.value = value // 给value新增一个__ob__属性,值为该value的Observer实例 // 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作 def(value,'__ob__',this) if (Array.isArray(value)) { // 当value为数组时的逻辑 // ... } else { this.walk(value) } } // 遍历对象中可枚举的属性,对每个属性进行"可观测"绑定 walk (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } } /** * 使一个对象转化成可观测对象 * @param { Object } obj 对象 * @param { String } key 对象的key * @param { Any } val 对象的某个key的值 */ function defineReactive (obj,key,val) { // 如果只传了obj和key,那么val = obj[key] if (arguments.length === 2) { val = obj[key] } if(typeof val === 'object'){ // 递归为对象类型添加监听 new Observer(val) } Object.defineProperty(obj, key, { enumerable: true, configurable: true, get(){ console.log(`${key}属性被读取了`); return val; }, set(newVal){ if(val === newVal){ return } console.log(`${key}属性被修改了`); val = newVal; } }) } /** * @param { Object } obj 对象 * @param { String } key 添加的属性名 * @param { Any } val 属性值 * @param { Boolean } 是否为可枚举属性 */ function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
- 上面的代码中定义了observer类,它用来将一个正常的object转换成可观测的object,并且给value新增一个__ob__属性,值为该value的Observer实例。这个操作相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作。
- 然后判断数据的类型,只有object类型的数据才会调用walk将每一个属性转换成getter/setter的形式来侦测变化。
- 最后,在defineReactive中当传入的属性值还是一个object时使用new observer(val)来递归子属性,这样我们就可以把obj中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。
- 现在我们可以这样定义car对象进行监听,如下:
let car = new Observer({ 'brand':'BMW', 'price':3000 })
- 这样就可以完成对car对象变得"可观测"了
3.依赖收集
- 什么是依赖收集?
- 从上面解析中让object数据变的可观测以后,我们就能知道数据什么时候发生了变化,但问题是当数据发生变化时,都去通知视图更新而视图那么大总不能一个数据变化了,把整个视图全部更新一遍所以合理的做法是视图里谁用到了这个数据就更新谁,换个优雅说法就是:我们把"谁用到了这个数据"称为"谁依赖了这个数据",我们给每个数据都建一个依赖数组(因为一个数据可能被多处使用),谁依赖了这个数据(即谁用到了这个数据)我们就把谁放入这个依赖数组中,那么当这个数据发生变化的时候,我们就去它对应的依赖数组中,把每个依赖都通知一遍,告诉他们:“你们依赖的数据变啦,你们该更新啦!”。这个过程就是依赖收集。
- 何时收集依赖?
- 谁用到了这个数据,那么当这个数据变化时就通知谁。所谓谁用到了这个数据,其实就是谁获取了这个数据,而可观测的数据被获取时会触发getter属性,那么我们就可以在getter中收集这个依赖。
- 何时通知依赖更新?
- 当这个数据变化时会触发setter属性,那么我们就可以在setter中通知依赖更新。
- 把依赖收集到哪里?
- 如上所述我们给每个数据都建一个依赖数组,谁依赖了这个数据我们就把谁放入这个依赖数组中。但是单单用一个数组来存放依赖的话,功能好像有点欠缺并且代码过于耦合。所以我们应该将依赖数组的功能扩展一下,更好的做法是我们应该为每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来。OK,到这里,我们的依赖管理器Dep类应运而生,代码如下:
// 源码位置:src/core/observer/dep.js export default class Dep { constructor () { // subs数组用来存放依赖 this.subs = [] } // 定义了以下几个实例方法用来对依赖进行添加,删除,通知等操作 addSub (sub) { this.subs.push(sub) } // 删除一个依赖 removeSub (sub) { remove(this.subs, sub) } // 添加一个依赖 depend () { if (window.target) { this.addSub(window.target) } } // 通知所有依赖更新 notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() // 通知更新 } } } /** * Remove an item from an array * 从依赖数组中移除依赖 */ export function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } }
- 有了依赖管理器后,我们就可以在getter中收集依赖,在setter中通知依赖更新了,代码如下:
function defineReactive (obj,key,val) { if (arguments.length === 2) { val = obj[key] } if(typeof val === 'object'){ new Observer(val) } //实例化一个依赖管理器,生成一个依赖管理数组dep const dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get(){ // 在getter中收集依赖 dep.depend() return val; }, set(newVal){ if(val === newVal){ return } val = newVal; // 在setter中通知依赖更新 dep.notify() } }) }
- 如上所述我们给每个数据都建一个依赖数组,谁依赖了这个数据我们就把谁放入这个依赖数组中。但是单单用一个数组来存放依赖的话,功能好像有点欠缺并且代码过于耦合。所以我们应该将依赖数组的功能扩展一下,更好的做法是我们应该为每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来。OK,到这里,我们的依赖管理器Dep类应运而生,代码如下:
4.依赖是谁
- 经过一步步解析我们明白了什么是依赖?何时收集依赖?以及收集的依赖存放到何处?那么我们收集的依赖到底是谁?
- 其实在Vue中实现了一个叫做Watcher的类,而Watcher类的实例就是我们上面所说的那个"谁"。换句话说就是:谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例。在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的Watch实例,由Watcher实例去通知真正的视图 。
- Watcher类的具体实现如下:
export default class Watcher { // constructor (vm,expOrFn,cb) { this.vm = vm; this.cb = cb; // 获取到一个更新回调函数或空值指引 this.getter = parsePath(expOrFn) this.value = this.get() } get () { // 把实例自身赋给了全局的一个唯一对象window.target上 window.target = this; const vm = this.vm // 获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter // call方法调用getter,第一个参数为指定上下文中的this,后面的参数才是方法参数 let value = this.getter.call(vm, vm) // 在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉 window.target = undefined; return value // } // 当数据变化时,会触发数据的setter,在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数,从而更新视图 update () { const oldValue = this.value this.value = this.get() this.cb.call(this.vm, this.value, oldValue) } } /** * Parse simple path. * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来 * 例如: * data = {a:{b:{c:2}}} * parsePath('a.b.c')(data) // 2 */ const bailRE = /[^\w.$]/ export function parsePath (path) { if (bailRE.test(path)) { return } // 以.为标记分割字符串 const segments = path.split('.') // 将函数指针返回出去,使得存储这些分割字符串能自由指定存储对象obj // 当前存储对象obj为vm return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] // obj经过每次覆盖不断触发getter依赖收集 } return obj // 返回最后被覆盖的值 } }
- Watcher类的执行流程如下:
- 首先当实例化Watcher类时,会先执行其构造函数;
- 在构造函数中调用了this.get()实例方法;
- 在get()方法中,首先通过window.target = this把实例自身赋给了全局的一个唯一对象window.target上,然后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉。
- 当数据变化时,会触发数据的setter,在setter中调用dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数,从而更新视图。
- 总结一下就是:Watcher先把自己设置到全局唯一的指定位置(window.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。
- 流程图如下:
5.不足之处
- 虽然我们通过Object.defineProperty方法实现了对object数据的可观测,但是这个方法仅仅只能观测到object数据的取值及设置值,当我们向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。
6.总结
-
首先,我们通过Object.defineProperty方法实现了对object数据的可观测,并且封装了Observer类,让我们能够方便的把object数据中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。
-
接着,我们学习了什么是依赖收集?并且知道了在getter中收集依赖,在setter中通知依赖更新,以及封装了依赖管理器Dep,用于存储收集到的依赖。
-
最后,我们为每一个依赖都创建了一个Watcher实例,当数据发生变化时,通知Watcher实例,由Watcher实例去做真实的更新操作。
-
流程总结如下:
- Data通过observer转换成了getter/setter的形式来追踪变化。
- 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
- 当数据发生了变化时,会触发setter,从而向Dep中的依赖(即Watcher)发送通知。
- Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。