系列文章
- [Vue源码学习] new Vue()
- [Vue源码学习] 配置合并
- [Vue源码学习] $mount挂载
- [Vue源码学习] _render(上)
- [Vue源码学习] _render(下)
- [Vue源码学习] _update(上)
- [Vue源码学习] _update(中)
- [Vue源码学习] _update(下)
- [Vue源码学习] 响应式原理(上)
- [Vue源码学习] 响应式原理(中)
- [Vue源码学习] 响应式原理(下)
- [Vue源码学习] props
- [Vue源码学习] computed
- [Vue源码学习] watch
- [Vue源码学习] 插槽(上)
- [Vue源码学习] 插槽(下)
前言
从上一章节中,我们知道,在初始化Vue实例的过程中,会将data选项中的普通数据转换成响应式数据,但是如果不进行访问或设置,是无法起到效果的,那么接下来,就来看看Vue是如何进行依赖收集的。
观察者模式与依赖收集
在看具体代码之前,我们需要了解在Vue中是如何运用观察者模式和依赖收集的。
在观察者模式中,目标对象会管理一个集合,里面保存着所有依赖于该目标对象的观察者,当目标对象发生变化时,会通知集合中所有的观察者进行更新。在Vue中,目标对象就是每一个经过defineReactive方法处理过的数据(也包含Observer对象本身),观察者就是一个个Watcher,它可以是渲染Watcher、计算Watcher、自定义Watcher等,而在调用defineReactive方法处理数据的过程中,会为每个数据创建一个Dep的实例,这个实例就是用来管理所有观察该目标对象的观察者集合,当数据发生变化时,Vue就会调用dep.notify方法,从而通知Dep中所有的观察者进行更新。
通过观察者模式,我们可以知道每个目标对象对应的观察者,而依赖收集解决的是每个观察者依赖了哪些数据,之所以这么设计,是为了在更新的过程中,通过重新计算观察者所依赖的目标数据,从而取消对多余的目标数据的观察,同时在目标数据的集合中移除该观察者,避免当目标数据再次发生变化时,产生额外的更新操作。那么接下来,就从源码的角度来看看Vue是如何实现该过程的。
从前面的$mount章节中,我们知道,每个组件实例都会创建一个渲染Watcher,在创建渲染Watcher的最后,会调用get方法,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
this.value = this.lazy
? undefined
: this.get()
}
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
}
/* core/observer/dep.js */
Dep.target = null
const targetStack = []
export function pushTarget(target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
可以看到,在get方法中,首先会调用pushTarget方法,将当前Watcher赋值给Dep.target,然后调用getter方法,也就是在mountComponent方法中传入的updateComponent方法,在其中会调用组件的render渲染函数,而在创建VNode的过程中,如果需要访问data选项中的数据,那么就会触发数据的get访问器:
/* core/observer/index.js */
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
// ...
}
})
}
可以看到,当触发get访问器时,这里的Dep.target指向的就是渲染Watcher,所以此时会调用此数据关联的dep的depend方法,进行依赖收集,代码如下所示:
/* core/observer/dep.js */
export default class Dep {
addSub(sub: Watcher) {
this.subs.push(sub)
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
/* core/observer/watcher.js */
export default class Watcher {
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)
}
}
}
}
可以看到,depend方法并没有简单的调用addSub方法,将观察者添加到目标对象的集合中,而是继续调用watcher.addDep方法,在此方法中,首先将目标对象对应dep添加到watcher.newDeps中,这一步就是之前所说的依赖收集,如果检测到该dep不存在于上一次该观察者所依赖的watcher.depIds中,说明这是一份新的依赖数据,才会调用dep.addSub方法,将该watcher实例添加到目标对象的dep集合中,完成观察者模式中的观察操作。
所以在调用dep.depend方法的过程中,经过上面的步骤后,目标对象的dep中包含着Watcher实例,同时Watcher实例中同样也包含着目标对象的dep。在get访问器中,还有另一段逻辑,当childOb存在时,会继续调用childOb.dep.depend方法,从上一小节中可以知道,假如调用defineReactive方法,且对应的数据还是对象时,会继续调用observe方法,将嵌套对象转换为响应式对象,所以这里的childOb就是嵌套对象的Observer实例,这里的childOb.dep就是Observer实例对应的dep,在调用depend方法后,相当于将嵌套对象也添加到当前Watcher中,建立两者之间的联系,所以直接修改嵌套对象时(this.obj.nested = {}),同样可以触发更新操作。
当render渲染函数执行完成后,当前组件的渲染Watcher就可以知道,它依赖了哪些数据,而这些数据中也保存有该渲染Watcher的引用。在调用完getter方法后,会调用popTarget方法,恢复Dep.target的引用,最后还会调用cleanupDeps方法,这个方法就是用来取消对多余的目标对象的观察,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
}
在看cleanupDeps方法之前,需要了解Watcher对象中的deps、newDeps、depIds、newDepIds四个属性,在Watcher每次进行重新渲染之前,此时的newDeps、newDepIds是空的,而deps、depIds保存的是上一次渲染所依赖的数据,在渲染的过程中,newDeps和newDepIds会收集本次渲染所依赖的数据,同时将新增的数据添加到deps和depIds中,在渲染完成后,就会调用cleanupDeps方法,可以看到,此时deps、depIds中的数据可能会比newDeps、newDepIds中的数据多,这部分多出来的数据,其实就是该Watcher不需要依赖的数据,需要进行移除。
在cleanupDeps方法中,首先遍历deps对象中的所有数据,如果检测在newDepIds中不存在,那么就调用dep.removeSub(this),从目标数据中的dep中移除当前Watcher,这样一来,当该数据再次发生变化时,就不会通知该Watcher了,然后交换deps与newDeps、depIds与newDepIds对象,然后将newDeps、newDepIds置空,这样就满足了在下次重新渲染时,newDeps、newDepIds是空的,deps、depIds保存的是上一次渲染所依赖的数据。
总结
Vue通过观察者模式和依赖收集,实现了在每次重新渲染的过程中,重新收集本次渲染所依赖的数据,这样一来,当依赖的数据发生变化时,就可以准确的通知相关的Watcher进行更新操作。