vue核心之响应式原理(双向绑定/数据驱动)
实例化一个vue对象时, Observer类将每个目标对象(即data)的键值转换成getter/setter形式,用于进行依赖收集以及调度更新。
Observer
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 /* 将Observer实例绑定到data的__ob__属性上面去,之前说过observe的时候会先检测是否已经有__ob__对象存放Observer实例了,def方法定义可以参考/src/core/util/lang.js*/ def(value, '__ob__', this) if (Array.isArray(value)) { /*如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。*/ const augment = hasProto ? protoAugment /*直接覆盖原型的方法来修改目标对象*/ : copyAugment /*定义(覆盖)目标对象或数组的某一个方法*/ augment(value, arrayMethods, arrayKeys) /*如果是数组则需要遍历数组的每一个成员进行observe*/ this.observeArray(value) } else { /*如果是对象则直接walk进行绑定*/ this.walk(value) }, walk (obj: Object) { const keys = Object.keys(obj) /*walk方法会遍历对象的每一个属性进行defineReactive绑定*/ for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } }
1. 首先将Observer实例绑定到data的ob属性上,防止重复绑定;
2. 若data为数组,先实现对应的变异方法(不做理解),再将数组的每个对象进行Observer,使之成为响应式数据;
3. 若data为对象,直接调用walk()方法,遍历对象的所有属性,进行getter/setter的绑定,核心方法为defineReactive();
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: Function ) { /*在闭包中定义一个dep对象*/ const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } /*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/ // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set /*对象的子对象递归进行observe并返回子节点的Observer对象*/ let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { /*如果原本对象拥有getter方法则执行*/ const value = getter ? getter.call(obj) : val if (Dep.target) { /*进行依赖收集*/ dep.depend(); if (childOb) { /*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/ childOb.dep.depend() } if (Array.isArray(value)) { /*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/ dependArray(value) } } return value }, set: function reactiveSetter (newVal) { /*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/ const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { /*如果原本对象拥有setter方法则执行setter*/ setter.call(obj, newVal) } else { val = newVal } /*新的值需要重新进行observe,保证数据响应式*/ childOb = observe(newVal) /*dep对象通知所有的观察者*/ dep.notify(); } }) }
其中getter方法:
1. 先为每一个data声明一个Dep实例对象,用于执行dep.depend()方法收集相关依赖;
2. 根据dep.target判断是否收集依赖;
那么问题来了,我们为什么要收集相关依赖呢??
new Vue({ template: `<div> <span>text1:</span> {{text1}} <span>text2:</span> {{text2}} <div>`, data: { text1: 'text1', text2: 'text2', text3: 'text3' } });
上述代码中,data中的text3并没有被实际使用,为了提高代码效率,我们没有必要进行响应式处理,因此,依赖收集简单点理解就是收集只在实际页面中用到的data数据,然后打上标记,这里就是标记为Dep.target。
在setter中:
1. 获取新的值并进行observer,保证数据响应式;
2. 通过dep.notify()方法通知watcher更新数据;
在defineReactive()方法中,我们可以看到在getter时,dep会收集相关依赖,即收集依赖的watcher,然后在setter操作时候通过dep去通知watcher,此时watcher就执行变化,我们用一张图描述这三者之间的关系:
从图我们可以简单理解:Dep可以看做是书店,Watcher就是书店订阅者,而Observer就是书店的书,订阅者在书店订阅书籍,就可以添加订阅者信息,一旦有新书就会通过书店给订阅者发送消息。
Watcher
Watcher是一个观察者对象。依赖收集以后Watcher对象会被保存在Dep的subs中,数据变动的时候Dep会通知Watcher实例,然后由Watcher实例回调cb进行视图的更新。
export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm /*_watchers存放订阅者实例*/ vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set()this.value = this.lazy ? undefined : this.get() } /*获得getter的值并且重新进行依赖收集*/ get () { /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/ pushTarget(this) let value const vm = this.vm /*执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。 在将Dep.target设置为自生观察者实例以后,执行getter操作。 譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c, 那么在执行getter的时候就会触发a跟c两个数据的getter函数, 在getter函数中即可判断Dep.target是否存在然后完成依赖收集, 将该观察者对象放入闭包中的Dep的subs中去。*/ if (this.user) { try { value = this.getter.call(vm, vm) } catch (e) { handleError(e, vm, `getter for watcher "${this.expression}"`) } } else { value = this.getter.call(vm, vm) } /*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/ if (this.deep) { /*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/ traverse(value) } /*将观察者实例从target栈中取出并设置给Dep.target*/ popTarget() this.cleanupDeps() return value } /*添加一个依赖关系到Deps集合中*/ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } /*清理依赖收集*/ cleanupDeps () { /*移除所有观察者对象*/ ... } /* 调度者接口,当依赖发生改变的时候进行回调。 */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { /*同步则执行run直接渲染视图*/ this.run() } else { /*异步推送到观察者队列中,下一个tick时调用。*/ queueWatcher(this) } } /* 调度者工作接口,将被调度者回调。 */ run () { if (this.active) { /* get操作在获取value本身也会执行getter从而调用update更新视图 */ const value = this.get() if ( value !== this.value || /* 即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。 */ isObject(value) || this.deep ) { // set new value const oldValue = this.value /*设置新的值*/ this.value = value /*触发回调*/ if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } } /*获取观察者的值*/ evaluate () { this.value = this.get() this.dirty = false } /*收集该watcher的所有deps依赖*/ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } /*将自身从所有依赖收集订阅列表删除*/ teardown () { ... } }
Dep
被Observer的data在触发 getter 时,Dep 就会收集依赖的 Watcher ,其实 Dep 就像刚才说的是一个书店,可以接受多个订阅者的订阅,当有新书时即在data变动时,就会通过 Dep 给 Watcher 发通知进行更新。
export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } /*添加一个观察者对象*/ addSub (sub: Watcher) { this.subs.push(sub) } /*移除一个观察者对象*/ removeSub (sub: Watcher) { remove(this.subs, sub) } /*依赖收集,当存在Dep.target的时候添加观察者对象*/ depend () { if (Dep.target) { Dep.target.addDep(this) } } /*通知所有订阅者*/ notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
总结
- 在 Vue 中模板编译过程中的指令或者数据绑定都会实例化一个 Watcher 实例,实例化过程中会触发 get()将自身指向 Dep.target;
- data在 Observer 时执行 getter 会触发 dep.depend() 进行依赖收集;依赖收集的结果:1、data在 Observer 时闭包的dep实例的subs添加观察它的 Watcher 实例;2. Watcher 的deps中添加观察对象 Observer 时的闭包dep;
- 当data中被 Observer 的某个对象值变化后,执行notify()方法触发subs中观察它的watcher执行 update() 方法,最后实际上是调用watcher的回调函数cb,进而更新视图。