收集的目的就是为了当咱们修改数据的时候,能够对相关的依赖派发更新,那么这一节咱们来详细分析这个过程。react
setter 部分的逻辑:express
/** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, // ... set: function reactiveSetter (newVal) { 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.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
假设咱们有以下模板:性能优化
<div id="demo"> {{name}} </div>
咱们知道这段模板将会被编译成渲染函数,接着建立一个渲染函数的观察者,从而对渲染函数求值,在求值的过程当中会触发数据对象 name 属性的 get 拦截器函数,进而将该观察者收集到 name 属性经过闭包引用的“筐”中,即收集到 Dep 实例对象中。这个 Dep 实例对象是属于 name 属性自身所拥有的,这样当咱们尝试修改数据对象 name 属性的值时就会触发 name 属性的 set 拦截器函数,这样就有机会调用 Dep 实例对象的 notify 方法,从而触发了响应,以下代码截取自 defineReactive 函数中的 set 拦截器函数:闭包
set: function reactiveSetter (newVal) { // 省略... dep.notify() }
如上高亮代码所示,能够看到当属性值变化时确实经过 set 拦截器函数调用了 Dep 实例对象的 notify 方法,这个方法就是用来通知变化的,咱们找到 Dep 类的 notify 方法,以下:异步
export default class Dep { // 省略... constructor () { this.id = uid++ this.subs = [] } // 省略... notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
你们观察 notify 函数能够发现其中包含以下这段 if 条件语句块:async
if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) }
对于这段代码的做用,咱们会在本章的 同步执行观察者 一节中对其详细讲解,如今你们能够彻底忽略,这并不影响咱们对代码的理解。若是咱们去掉如上这段代码,那么 notify 函数将变为:函数
notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
notify 方法只作了一件事,就是遍历当前 Dep 实例对象的 subs 属性中所保存的全部观察者对象,并逐个调用观察者对象的 update 方法,这就是触发响应的实现机制,那么你们应该也猜到了,从新求值的操做应该是在 update 方法中进行的,那咱们就找到观察者对象的 update 方法,看看它作了什么事情,以下:oop
update () { /* istanbul ignore else */ if (this.computed) { // 省略... } else if (this.sync) { this.run() } else { queueWatcher(this) }
在 update 方法中代码被拆分红了三部分,即 if...else if...else 语句块。首先 if 语句块的代码会在判断条件 this.computed 为真的状况下执行,咱们说过 this.computed 属性是用来判断该观察者是否是计算属性的观察者,这部分代码咱们将会在计算属性部分详细讲解。也就是说渲染函数的观察者确定是不会执行 if 语句块中的代码的,此时会继续判断 else...if 语句的条件 this.sync 是否为真,咱们知道 this.sync 属性的值就是建立观察者实例对象时传递的第三个选项参数中的 sync 属性的值,这个值的真假表明了当变化发生时是否同步更新变化。对于渲染函数的观察者来说,它并非同步更新变化的,而是将变化放到一个异步更新队列中,也就是 else 语句块中代码所作的事情,即 queueWatcher 会将当前观察者对象放到一个异步更新队列,这个队列会在调用栈被清空以后按照必定的顺序执行。关于更多异步更新队列的内容咱们会在后面单独讲解,这里你们只须要知道一件事情,那就是不管是同步更新变化仍是将更新变化的操做放到异步更新队列,真正的更新变化操做都是经过调用观察者实例对象的 run 方法完成的。因此此时咱们应该把目光转向 run 方法,以下:性能
run () { if (this.active) { this.getAndInvoke(this.cb) } }
run 方法的代码很简短,它判断了当前观察者实例的 this.active 属性是否为真,其中 this.active 属性用来标识一个观察者是否处于激活状态,或者可用状态。若是观察者处于激活状态那么 this.active 的值为真,此时会调用观察者实例对象的 getAndInvoke 方法,并以 this.cb 做为参数,咱们知道 this.cb 属性是一个函数,咱们称之为回调函数,当变化发生时会触发,可是对于渲染函数的观察者来说,this.cb 属性的值为 noop,即什么都不作优化
如今咱们终于找到了更新变化的根源,那就是 getAndInvoke 方法,以下:
getAndInvoke (cb: Function) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value this.dirty = false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) } } }
在 getAndInvoke 方法中,第一句代码就调用了 this.get 方法,这意味着从新求值,这也证实了咱们在上一小节中的假设。对于渲染函数的观察者来说,从新求值其实等价于从新执行渲染函数,最终结果就是从新生成了虚拟DOM并更新真实DOM,这样就完成了从新渲染的过程。在从新调用 this.get 方法以后是一个 if 语句块,实际上对于渲染函数的观察者来说并不会执行这个 if 语句块,由于 this.get 方法的返回值其实就等价于 updateComponent 函数的返回值,这个值将永远都是 undefined。实际上 if 语句块内的代码是为非渲染函数类型的观察者准备的,它用来对比新旧两次求值的结果,当值不相等的时候会调用经过参数传递进来的回调。咱们先看一下判断条件,以下:
const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // 省略... }
首先对比新值 value 和旧值 this.value 是否相等,只有在不相等的状况下才须要执行回调,可是两个值相等就必定不执行回调吗?未必,这个时候就须要检测第二个条件是否成立,即 isObject(value),判断新值的类型是不是对象,若是是对象的话即便值不变也须要执行回调,注意这里的“不变”指的是引用不变,以下代码所示:
const data = { obj: { a: 1 } } const obj1 = data.obj data.obj.a = 2 const obj2 = data.obj console.log(obj1 === obj2) // true
上面的代码中因为 obj1 与 obj2 具备相同的引用,因此他们老是相等的,但其实数据已经变化了,这就是判断 isObject(value) 为真则执行回调的缘由。
接下来咱们就看一下 if 语句块内的代码:
const oldValue = this.value this.value = value this.dirty = false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) }
代码若是执行到了 if 语句块内,则说明应该执行观察者的回调函数了。首先定义了 oldValue 常量,它的值是旧值,紧接着使用新值更新了 this.value 的值。咱们能够看到如上代码中是如何执行回调的:
cb.call(this.vm, value, oldValue)
将回调函数的做用域修改成当前 Vue 组件对象,而后传递了两个参数,分别是新值和旧值。
另外你们可能注意到了这句代码:this.dirty = false,将观察者实例对象的 this.dirty 属性设置为 false,实际上 this.dirty 属性也是为计算属性准备的,因为计算属性是惰性求值,因此在实例化计算属性的时候 this.dirty 的值会被设置为 true,表明着尚未求值,后面当真正对计算属性求值时,也就是执行如上代码时才会将 this.dirty 设置为 false,表明着已经求过值了。
除此以外,咱们注意以下代码:
if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) }
在调用回调函数的时候,若是观察者对象的 this.user 为真意味着这个观察者是开发者定义的,所谓开发者定义的是指那些经过 watch 选项或 $watch 函数定义的观察者,这些观察者的特色是回调函数是由开发者编写的,因此这些回调函数在执行的过程当中其行为是不可预知的,极可能出现错误,这时候将其放到一个 try...catch 语句块中,这样当错误发生时咱们就可以给开发者一个友好的提示。而且咱们注意到在提示信息中包含了 this.expression 属性,咱们前面说过该属性是被观察目标(expOrFn)的字符串表示,这样开发者就能清楚的知道是哪里发生了错误。
异步更新的意义----性能优化
当全部的突变完成以后,再一次性的执行队列中全部观察者的更新方法,同时清空队列,这样就达到了优化的目的
看一看其具体实现,咱们知道当修改一个属性的值时,会经过执行该属性所收集的全部观察者对象的 update 方法进行更新,那么咱们就找到观察者对象的 update 方法,以下:
update () { /* istanbul ignore else */ if (this.computed) { // 省略... } else if (this.sync) { this.run() } else { queueWatcher(this) } }
若是没有指定这个观察者是同步更新(this.sync 为真),那么这个观察者的更新机制就是异步的,这时当调用观察者对象的 update 方法时,在 update 方法内部会调用 queueWatcher 函数,并将当前观察者对象做为参数传递,queueWatcher 函数的做用就是咱们前面讲到过的,它将观察者放到一个队列中等待全部突变完成以后统一执行更新。
queueWatcher 函数来自 src/core/observer/scheduler.js 文件,以下是 queueWatcher 函数的所有代码:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
queueWatcher 函数接收观察者对象做为参数,首先定义了 id 常量,它的值是观察者对象的惟一 id,而后执行 if 判断语句,以下是简化的代码:
export function queueWatcher (watcher: Watcher) { const id = watcher.id // let has: { [key: number]: ?true } = {} if (has[id] == null) { has[id] = true // 省略... } }
当 queueWatcher 函数被调用以后,会尝试将该观察者放入队列中,并将该观察者的 id 值登记到 has 对象上做为 has 对象的属性同时将该属性值设置为 true。该 if 语句以及变量 has 的做用就是用来避免将相同的观察者重复入队的。在该 if 语句块内执行了真正的入队操做,以下代码高亮的部分所示:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // let flushing = false 初始值是 false if (!flushing) { // const queue: Array<Watcher> = [] queue.push(watcher) } else { // 省略... } // 省略... } }
flushing 变量是一个标志,咱们知道放入队列 queue 中的全部观察者将会在突变完成以后统一执行更新,当更新开始时会将 flushing 变量的值设置为 true,表明着此时正在执行更新,因此根据判断条件 if (!flushing) 可知只有当队列没有执行更新时才会简单地将观察者追加到队列的尾部有的同窗可能会问:“难道在队列执行更新的过程当中还会有观察者入队的操做吗?”,其实是会的,典型的例子就是计算属性,好比队列执行更新时常常会执行渲染函数观察者的更新,渲染函数中极可能有计算属性的存在,因为计算属性在实现方式上与普通响应式属性有所不一样,因此当触发计算属性的 get 拦截器函数时会有观察者入队的行为,这个时候咱们须要特殊处理,也就是 else 分支的代码,以下:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // 省略... } }
当变量 flushing 为真时,说明队列正在执行更新,这时若是有观察者入队则会执行 else 分支中的代码,这段代码的做用是为了保证观察者的执行顺序,如今你们只须要知道观察者会被放入 queue 队列中便可
接着咱们再来看以下代码:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // 省略... // queue the flush > if (!waiting) { > waiting = true > if (process.env.NODE_ENV !== 'production' && !config.async) { > flushSchedulerQueue() > return > } > nextTick(flushSchedulerQueue) > } } }
你们观察如上代码中有这样一段 if 条件语句:
if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return }
在接下来的讲解中咱们将会忽略这段代码,并在 同步执行观察者 一节中补充讲解,
咱们回到那段高亮的代码,这段代码是一个 if 语句块,其中变量 waiting 一样是一个标志,它也定义在 scheduler.js 文件头部,初始值为 false
let waiting = false
咱们看 if 语句块内的代码就知道了,在 if 语句块内先将 waiting 的值设置为 true,这意味着不管调用多少次 queueWatcher 函数,该 if 语句块的代码只会执行一次。接着调用 nextTick 并以 flushSchedulerQueue 函数做为参数,其中 flushSchedulerQueue 函数的做用之一就是用来将队列中的观察者统一执行更新的。对于 nextTick 相信你们已经很熟悉了,其实最好理解的方式就是把 nextTick 看作 setTimeout(fn, 0),以下:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true // 省略... // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } setTimeout(flushSchedulerQueue, 0) } } }
咱们对 Vue 数据修改派发更新的过程也有了认识,实际上就是当数据发生变化的时候,触发 setter 逻辑,把在依赖过程当中订阅的的全部观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列作了进一步优化,在 nextTick 后执行全部 watcher 的 run,最后执行它们的回调函数。nextTick 是 Vue 一个比较核心的实现了。