在《响应式原理一:data 初始化》一文中,分析了 data 是如何将一个普通对象变成响应式对象,核心的实现是在函数 defineReactive 中采用 ES5 Object.defineProperty 定义了 get 和 set 函数,其作用是依赖收集和派发更新。那么,本文将分析依赖收集的实现原理。
何时触发 get
在开始之前,先来看看 get 函数的触发时机。
在实例化 Vue,即执行 new Vue 时,不管是用户手动挂载 Vue,还是框架内部挂载 Vue,都会调用到函数 $mount,其内部会调用函数 mountComponent。在该函数的内部实现,有这么一段逻辑:
export function mountComponent {
......
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
......
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
......
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
}
需要注意的两点:
一是定义函数 updateComponent;
二是实例化 Watcher,此时是一个渲染 Watcher。
在实例化渲染 Watcher 的过程中,会触发 get 函数,即
/**
* Evaluate the getter, and re-collect dependencies.
*/
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
}
需要关注的核心代码:this.getter.call(vm, vm),此时会触发 updateComponent 函数被调用。在执行该函数的过程中,会先执行 render 函数;那么此时会访问数据对象,从而触发响应式对象属性 get 函数调用。
依赖收集
了解了其触发时机,那么就可以来看看是如何实现依赖收集的?代码实现如下:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
......
let childOb = !shallow && observe(val)
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
}
})
......
}
在触发之前,会先实例化 Dep,为什么需要这样做呢?因为 Dep 是整个依赖收集的核心,简单来说,它是一个订阅中心,需要关注它的变化的观察者可以进行订阅;那么当这个订阅中心发生变化时,则会通知观察者做出相应的改变,采用的是观察者模式。
Dep 有一个静态属性:target,它是一个全局唯一 Watcher,因为在同一时间只能有一个全局的 Watcher 被计算。
接着会调用 dep 方法 depend,其内部实现如下:
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
由于 Dep.target 保存的是 Watcher 实例,那么 Dep.target.addDep(this) 其实是指调用 watcher 对象方法 addDep,this 是指 dep 对象,其内部实现如下:
/**
* Add a dependency to this directive.
*/
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)
}
}
}
这里需要注意有 4 个变量:newDepIds、depIds、newDepIds、depIds,它们是在实例化 Watcher 定义的,如下:
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
deps 和 newDeps 其数据类型都是数组,表示 Watcher 实例持有 Dep 实例的数组。具体来说,deps 表示上一次添加的 Dep 实例数组,newDeps 表示新添加的 Dep 实例数组。
depIds 和 newDepIds 其数据结构是 Set,分别代表 deps 和 newDeps 的 id。
这里会做一些逻辑判断,作用是防止同一条数据被添加多次。然后会调用实例 dep 方法 addSub,即把当前 wacher 添加到订阅列表里 subs,作用是当数据变化时,通过遍历 subs 通知每个 sub,其内部实现如下:
addSub (sub: Watcher) {
this.subs.push(sub)
}
回到依赖收集函数 get,执行完实例 dep 方法 depend后,还有一段逻辑,即当 childOb 不为空的时候,会执行 if 逻辑,作用是针对嵌套对象。
由于 childOb 是 Observer 实例,那么 childOb.dep.depend() 其实是指调用实例 dep 方法 depend,已分析过。
接着,如果 value 是数组的话,则会调用 dependArray,其内部实现如下:
/**
* Collect dependencies on array elements when the array is touched, since
* we cannot intercept array element access like property getters.
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
实现逻辑也挺简单的,对 value 进行遍历,如果满足条件的话,最终还是调用 dep 实例方法 depend。除此之外,如果遍历到的元素是数组的话,则递归调用 dependArray,执行相同的逻辑,直到处理完毕为止。