Vue.js 中 deep-waching 的实现

2016.12.21

先来看看 Vue 中 $watch 方法的 API 的基本用法

1
2
3
4
5
6
7
8
9
10
11
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
})
// 为了发现对象内部值的变化,可以在选项参数中指定 deep: true
vm.$watch('someObject', callback, {
deep: true
})
vm.someObject.nestedValue = 123
// callback is fired

可以看到 Vue.js 是可以针对某一层级的属性添加回调的,并且可以通过 options 配置是否深度观察。

当 Watcher被实例化后(比如通过 vm.$watch('someprop', callback)), 都会在实例化的过程中调用this.get 函数以初始化被观测对象的值:

1
2
3
this.value = this.lazy
? undefined
: this.get()

this.get函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
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)
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}

这是 Vue 中 Watcher 对象的 get 方法,Vue 通过这个 get 方法调用 getter 函数,而 getter 其实是一个闭包,由函数 parsePath 函数返回,parsePath函数接收一个键路径为参数。

1
2
3
4
5
6
7
8
9
10
function parsePath (path: string): any {
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}

那调用了 getter 有有什么用呢,其实这里就会触发 Vue 中最最核心的部分 Object.defineProperty 中的 get 函数中的 dep.depend(), 接着dep.depend 就会往自身添加调用 getter 函数所指定的全局Dep.target

好现在说说 deep watching,可以看到 get 函数中有这样一段

1
2
3
4
5
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}

这是调用的 traverse 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
const seenObjects = new Set()
function traverse (val: any) {
seenObjects.clear()
_traverse(val, seenObjects)
}
function _traverse (val: any, seen: Set) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}

裸眼看上去这个函数似乎没有任何副作用,但其实,关键在这里:val[keys[i]]traverse函数借此依次向当前观测的属性值(前提是一个对象)的子属性挨个”蹭”一下,蹭起来就和之前 getter 的套路差不多,同样是收集依赖。也就是说,平时每一个特定深度的属性在 deep:false 的时候会沿路径收集依赖,直到收集到指定的属性为止,而当deep:tree 的时候,还会向下更深的属性继续收集依赖,这样当深级属性的set 函数被触发的时候,会调用所有上级属性的回调,也就是所谓的deep watching