写过上一次的 Vue 响应式数据构造过程 的文章后,感觉确实能帮助理解不少。所以再接再厉,撸完之前避开没有谈及的 Watcher(观察者)构造的过程。
预备知识
同样先给出一些预备的知识点,尤其是响应式数据构造的知识,因为 watcher 的实现中会有发生交集的地方。
$watch
我是从 Vue.prototype.$watch 这个 API 切入的。实际上 .vue 文件中的 watch 选项也是通过遍历对象属性,然后依次调用 $watch 实现的。
1 2 3 4 5 6 7
| Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { }
|
我们先看一下这里三个参数:
expOrFn: 我们观察的对象。如果是字符串,代表某个 obj 上的属性的键路径;如果是函数,代表计算属性,(tip: 运行时已经绑定了 vm 为 this 的志向,因此不可以是箭头函数)。
cb:回调函数,用于在 watcher 接收到更新信息后执行的函数,就是 watch 内定义的函数。
options:观察者创建时的配置项,有 deep、immediate 等选项。
1 2 3 4 5 6 7 8 9 10 11 12 13
| const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() }
|
再看下内部的实现:
我们先判断参数 cb 是不是对象,如果是对象的话则调用 createWatcher 函数,这里这个函数非常简单,只是用于寻找一个真正为函数的 cb,然后重新调用 vm.$watch 方法,确保 cb 被执行时一定是函数。这里如果 cb 是对象则会寻找 handler 属性的值。如果 cb 是字符串的话,则会选择 vm 实例上已经注册了的方法(列如:method 内注册的方法)。
options.user
属性是用于区分生产环境和开发环境下一些警告语句的输出。
创建了一个观察者的实例。
如果配置项 options.immediate
为 true,则立即以观察的对象的初始值来执行回调函数。
最后返回的一个工具函数,在官网 API 里面有提及,是用于取消观察者的工具,可以让一个 watcher 实例失效。
Class Watcher
这里我们将类里面的每个方法都逐个看一下。
constructor
1 2
| this.vm = vm vm._watchers.push(this)
|
在当前 Vue 的实例 vm._wathcers
上收集这个观察者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid this.active = true this.dirty = this.lazy this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
|
初始化 options 上的各项参数(用户未配置则初始化为 false):
- deep 决定
wathcer.value
上的值是否深度观察。
- user 区分某些情况下是否应给出警示或者错误提示。
- lazy 是否延迟或者 expOrFn 的值,如果延迟的话,必须等待外部调用
watcher.get
后才会获取 expOrFn 的值,然后赋值到 watcher.value
上。
- sync 决定是立即执行回调函数,还是进入更新队列,等待执行。
初始化 watcher 的 ID(ID:step 为 1 递增);
初始化 deps、newDeps、depIds、newDepIds,这些是用于记录该观察者都被哪些观察者容器收集了(或者说当前的观察者都观察了哪些 Observer),其中 deps 和 depIds 是真正用于收集的容器,而 newDeps 和 newDepIds 则类似于一个缓冲容器,当 cleanupDeps 执行时完成清空缓冲,赋值容器的工作;
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } }
|
之前有说过,如果 expOrFn 是键路径的字符串:
我们可以简单看一下 parsePath 的实现(util/lang.js)
1 2 3 4 5 6 7 8 9 10 11 12 13
| export function parsePath (path: string): any { if (bailRE.test(path)) { return } const segments = path.split('.') return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
|
这里很简单,我们先解析路径,随后利用闭包将 segments 推出parsePath 的生命周期,使其存在内存中,当闭包调用时,就可以调用这个路径数组,返回我们想要的值。
如果 expOrFn 本身就是计算属性的函数的话,就可以在调用时,直接返回一个值给我们。
如果 expOrFn 两者都不符合,则会给出警示。
1 2 3
| this.value = this.lazy ? undefined : this.get()
|
这里比较简单,就是判断是否需要延迟计算,如果不需要的话,就直接计算 expOrFn 的值,然后保存在 value 属性上。
get
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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) } if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() return value }
|
这里用于获取 expOrFn 的值。
- 首先我们将当前的观察者推送到 Dep.target 内,为了让 Vue 能够完成内部依赖收集的工作。(详见 defineReactive 的 getter 的实现)。
- 计算 getter,获取值。
- 如果 options.deep 为 true,则开启深度依赖收集。随后会深入介绍,这里和 observe 之间的互动比较隐晦。
- 从 Dep.target 推出当前的观察者。
- 同步容器。
- 返回 getter 计算值。
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
| const seenObjects = new Set() function traverse (val: any) { seenObjects.clear() _traverse(val, seenObjects) } function _traverse (val: any, seen: ISet) { 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) } }
|
这里简单来看,就是一次单纯的递归。但是 while (i--) _traverse(val[i], seen)
& while (i--) _traverse(val[keys[i]], seen)
是会触发我们之前在 defineReactive 中定义的 getter, 如下:
1 2 3 4 5 6 7 8 9 10 11 12
| if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } if (Array.isArray(value)) { dependArray(value) } }
|
因此 val 下的每个紫属性都会完成依赖收集,只要是 val 下的子属性发生变化时都会触发 val 的 update 以及回到函数的执行。
因此说这里有个非常隐晦的互动关系。
addDep
1 2 3 4 5 6 7 8 9 10 11
| 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) } } }
|
这里需要提前说明白的是:Dep 类 与 Watcher 类 的关系是 N 对 N 的关系。也就是两者之间的联系是如果你收集了我,我也必须收集你。
所以这里当要给 watcher 实例添加一个新的观察,则先收集它的观察者容器。然后让自己被这个观察者容器收集。
cleanupDeps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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 }
|
这里的工作其实就是同步一下 缓冲容器 和 真正容器。在真正容器中移除缓冲容器中不存在的对象,将缓冲容器中的值赋予真正容器,并清空缓冲容器。
update
1 2 3 4 5 6 7 8 9 10
| update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
|
这里就是接受 Observe 实例的更新通知的函数了,根据 sync 的值我们判断是立即更新还是进入更新队列,等待更新。
###
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
| run () { if (this.active) { const value = this.get() if ( value !== this.value || isObject(value) || this.deep ) { const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } }
|
这里就是执行 watcher 的回调,同时传入新旧值。
evaluate
1 2 3 4
| evaluate () { this.value = this.get() this.dirty = false }
|
暂时没遇见过这里的调用环境,不太明确,只能理解为获取 getter 的值。
depend
1 2 3 4 5 6
| depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } }
|
这其实就是一次从 watcher 开始触发的 Dep 和 Watcher 之间依赖互相收集的过程。
teardown
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| teardown () { if (this.active) { if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } }
|
这里就是一个 watcher 实例的取消工具,在 vm._watchers
上移除自己,同时通知 deps 中所有 dep 移除自己,然后清空 deps。
总结
以上就是一个 Watcher 的所有构造过程,以及工具函数的使用。自己仔细去读的话可以理解很多,一方面是如果架构这样一种非常大型的观察者模式,同时看看相关工具函数,学学高手的高级招数也是很不错的。
这里 Watcher 主要表现的业务场景还是 watch 这个选项。但是随着之后源码更多深入挖掘,应该会发觉更多的使用场景。😎