前言
前几天又复习了一下Vue2响应式原理,记下了一些笔记,对于正在看Vue2源码的同学应该有一些帮助。
内容主要包括两部分:双向绑定和Scheduler
双向绑定
- vue组件【A】始化,并初始化一个render Watcher,赋值给Dep.target。
- defineProperty一下data对象,首先会初始化一个
dep = new Dep()
,另外会代理setter、getter,返回一个observal的对象- 也就是说,每次调用defineProperty就会生成一个dep对象。也就是说,data、computed里面,有一个属性就会有一个dep对象。
- 这个observal的对象被【A】、【B】两个组件引用,并调用了observal对象的name属性
- 调用observal对象的name属性,触发getter函数,其执行dep.depend()
- dep有个subs的属性,它里面会存放watcher对象。而dep.depend()的代码如下:
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
它会用到Dep.target,而这个值取决于当前的Vue组件初始化的阶段。如果处于render阶段,那么这个Dep.target就是render Watcher。如果处于watch属性这种,那么Dep.target就是user Watcher。
- render Watcher其实就是这个watcher.run的时候的回调函数是去更新组件
- user Watcher这个watcher.run的时候其实是执行用户自定义的watch属性的回调函数
继续往下分析,这里Dep.target.addDep(this),他会做两件事:
- 把dep.id存到Dep.target这个watcher内部,好让watcher知道自己关联哪些dep了。即如果发现watcher已经关联某个dep了,就不会继续往下走了。
- 如果继续往下走,dep本身还会把watcher加入到dep.subs这个数组里
所以,当observal的某个属性被get,然后dep.depend()执行后,这个属性就会跟watcher绑定起来。
总结:
- watcher分为render watcher和user watcher;render watcher的回调函数用来更新组件;user watcher的回调函数用来执行watch属性的回调函数。
- 一个Vue组件至少有一个watcher
- 一个Vue组件的data和computed中的每一个属性,都对应一个dep对象,当getter调用时,通过dep.depend(),让dep对象关联上一系列watcher对象
- 一个dep可以对应多个watcher
- 一个watcher可以关联多个dep.id
注意,getter对象调用,可能是被本组件调用,也可能是被其他组件调用,所以dep.subs里面寸的watcher可能属于多个vue组件
- setter调用时,执行dep.notify(),它会遍历subs,依次执行watcher.update()
watcher属于各个vue组件,所以setter后,接下来就会执行各个vue组件的render callback或者user callback。
- 这个时候会有sheduler机制,在非sync模式下,watcher的callback不会立即执行,而是通过scheduler包下的queueWatcher来调度执行
调度Scheduler
调度的主要作用:
- 合并setter:当同一个dep.id,也就是同一个属性,被多次setter时,watcher的callback只有一个可以执行。
具体来说,当同一个dep.id被多次setter时,只有第一个watcher会被加入到任务队列。但是又因为watcher是一个mutable的对象,其实多个setter对应的都是同一个watcher引用。所以即使只是把第一个watcher加入到任务队列,也能拿到最新的数据。
- 任务队列排序,queueWatcher的时机不一定是按照watcher.id的顺序来的,而vdom里面,组件是父组件先渲染,然后子组件在渲染。所以,会把任务队列按照watcher.id升序排列,然后再去依次执行。以保证组件更新以预期的顺序执行。
调度的原理:
基于事件循环,从微观上看,就是在nextTick时flush掉任务队列;从宏观上看,就是用户在一个微任务里触发了多次setter,然后再nextTick时真正去执行watcher的回调函数。
具体看代码把:github.com/vuejs/vue/b…
NextTick原理:
对于Promise.resolve来说,每次执行相当于往微任务队列里加一个任务。 而对于nextTick,每次执行,会先把callback存到队列里,然后把清空队列的这个任务加入到微任务队列。
所以区别就是,nextTick会把多个任务合并到一个微任务中。
为什么nextTick的时候才能观测到视图变化。
new Vue({
// ...
methods: {
// ...
example: function () {
// 修改数据
this.message = 'changed'
// DOM 还没有更新
this.$nextTick(function () {
// DOM 现在更新了
// `this` 绑定到当前实例
this.doSomethingElse()
})
}
}
})
在上述代码里,微任务队列会依次加入两个任务:
- queueWatcher内部的flushQueue这个任务
- this.doSomethingElse()这个任务
微任务队列中的任务是依次执行的,第一个任务是flushQueue,flushQueue就是会调用watcher.run,run会同步去更新组件。也即是说,在下一个微任务开始前,flushQueue已经同步的把该更新的组件都更新了。