Watcher和Dep的实现解析
在 Vue2 中,Watcher 是 Vue 的核心,是实现 Vue 渲染,Computed, Watch 的关键支柱。
接下来将根据 Vue 的源码讲讲 Watcher 的逻辑,设计,与 Dep 是如何联系起来的。
# 一、Watcher,Dep 是什么
- Watcher(观察者): 在 Vue2 中,Watcher 是 Vue 的核心之一,负责处理渲染、计算属性和用户定义的 Watcher。每个 Vue 组件都有一个 Render Watcher,每个计算属性都有一个 Computed Watcher,每个 Watch 都有一个 User Watcher。
- Dep(依赖): 在 Vue 的初始化过程中,当对 data 进行响应式代理的时候,每个属性都会被设置一个 Dep 对象。Dep 的作用是关联 Watcher 和属性,当属性发生变化时通知相应的 Watcher。
# 二、各种 Watcher
# 1. Render Watcher - 渲染 Watcher
- 功能: 负责渲染组件。在 Vue 初始化实例时,会创建 Render Watcher,并将渲染函数赋值给 getter。
- 执行流程: Render Watcher 在初始化阶段会执行一次
updateComponent
函数,这个函数会调用_update(_render)
进入渲染流程。 - 关联 Dep: 在渲染过程中,如果访问了 data 的属性,该属性的 Dep 会收集 Render Watcher,同时 Render Watcher 也会收集该 Dep。
# 2. Computed Watcher - 计算 Watcher
- 功能: 负责计算属性的求值。Computed Watcher 在初始化时不会立即执行回调函数,而是在渲染时执行。
- 执行流程: 当 Computed Watcher 需要重新计算值时,会执行
evaluate
方法,这个方法会调用get
方法获取最新的值。 - 关联 Dep: Computed Watcher 会在获取值的时候收集依赖,同时通过
depend
方法将自己添加到相关的 Dep 中。
# 3. User Watcher - 用户 Watcher
- 功能: 负责执行用户定义的 Watcher。
- 执行流程: 当用户监听的属性发生变化时,User Watcher 会执行用户定义的回调函数。
- 关联 Dep: User Watcher 与 Render Watcher 的关系相似,会在初始化时建立依赖关系。
# 三、Dep 的实现与 Watcher 关系
初始化的时候,Vue 会将 data 改造成响应式代理,具体可以看这篇文章。当改造的过程中,Vue 会给 data 对象的每个属性都设置一个 Dep 对象。这个对象的作用是将 Watcher 与该属性关联起来。
比如说:
<template>
{{ a }}
</template>
<script>
export default {
data() {
return {
a: 1,
}
}
}
</script>
上方是一个 Vue 组件,那么它初始化的时候就会创建一个 Watcher 属性,在渲染阶段,会去读取a
属性,这个时候,a
属性就会创建一个 Dep 对象,并且将 Dep 对象收集 Render Watcher。当修改a
属性这个值的时候,Dep 对象将会通知 Render Watcher,让其开始渲染操作。
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
...
}
return isRef(value) && !shallow ? value.value : value
},
Watcher 和 Dep 是多对多的关系。每个 Watcher 可以有多个关联的 Dep,每个 Dep 也可以有多个关联的 Watcher
# 四、各种 Watcher 的实现
上面提及到,总共有三种 Watcher,每种 Watcher 目的都是不一样,但是实现上基本上一样,只是通过属性去区分。
Watcher 的公共属性
{
id: number; // Watcher的唯一id
deps: Set<Dep>; // Watcher收集的Dep对象
getter: string | Function; // Watcher所执行的回调 或者是 User Watcher所监听的属性
depIds: Set<number>; // Watcher dep对象的id,用于去重
active: Boolean; // 判断Watcher是否活跃
}
# Render Watcher
Render Watcher 就是最纯粹的 Watcher(可能刚开始设计的时候,只考虑做渲染),目的就是为了渲染组件。每个组件都有自己的一个 Render Watcher,在 Vue 初始化实例的时候,会创建 Render Watcher,并将 updateComponent
函数(就是执行 _update(_render)
来进入渲染流程)赋值给 getter 方法。
因为在初始化 Render Watcher 的过程中,会直接执行 getter
方法,那么就会执行 updateComponent
,如果在 template 模版中有使用 data 中的属性,那么在属性改造的 get 方法中就会调用 dep.depend
方法,Render Watcher 就会收集该 Dep,同样,Dep 也会收集该 Watcher。
那么在修改属性的时候,属性改造的 set 方法就会执行 dep.notify
就会触发 Render Watcher 的 update
方法,然后执行 getter
方法,下方截取一些 Vue 的源码
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
)
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
...
this.value = this.lazy ? undefined : this.get()
}
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e: any) {
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
}
# Computed Watcher
Computed Watcher 是所有 Watcher 中实现最复杂,最绕的。
再讲这个 Computed Watcher 之前,先讨论一下 Computed 属性,因为 Vue 对其也做了一个代理改造。跟 data 属性一样,对 computed 的属性使用 Object.defineProperty
改造。
这个改造的逻辑就是,如果 Computed Watcher 所依赖的 data
属性已经被修改时,Watcher 就会重新执行 Computed 函数来获取最新值,这个主要是解决视图多次获取 Computed 值时,仅执行函数一次,提高复用性。
同时,因为 Computed 对于视图 template 来说,相当于 data 的属性一样,也会获取 Computed 来渲染视图,所以 Computed 也有像 Dep,与其他类型的 Watcher 有关联。后面会介绍 Render Watcher 是如何跟 Computed Watcher 结合的。
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
};
}
讲完 Computed 属性的改造,回到 Computed Watcher,基于公共属性,Computed Watcher 添加了许多属性。
export interface DepTarget {
id: number
addDep(dep: Dep): void
update(): void
}
class Watcher implements DepTarget { // 赋予Dep的功能
constructor(options) {
// ...
this.lazy = options.lazy; //当是Computed Watcher的时候, 该值为true
this.dirty = this.lazy;
this.value = this.lazy ? undefined : this.get()
}
}
{
lazy: boolean, // 推迟执行
dirty: boolean, // 依赖修改
value: any, // computed的值
}
首先,如果用户有定义 Computed 的时候,Vue 将会为 Computed 下的所有属性都生成一个 Computed Watcher,这个 Watcher 不像 Render Watcher 马上执行回调函数,而是推后到视图渲染的时候再执行。
当 Vue 的 Render Watcher 开始渲染时,如果视图引用到 Computed 属性的时候,那么就会执行上方的代理函数 computedGetter
,并且执行 Watcher 的 evaluate
函数。
evaluate() {
this.value = this.get()
this.dirty = false
}
这个 get
方法跟上方的一样,这里解释一下 pushTarget
方法,因为有多种 Watcher,所以 Vue 将维护一条 Watcher 栈,将执行 getter 之前把该 Watcher 压入栈中,给后续 data 属性去依赖。回到渲染逻辑,因为开始执行 Render Watcher 的 getter
方法时,就已经把 Render Watcher 压入栈中,然后执行到 Computed Watcher 的时候,也将其压入栈中, 所以栈头目前是 Computed Watcher。
那么当获取 Data 的值时候, Data 的 Dep 和 Computed Watcher 就会相互收集。当 Data 的值被修改的时候,就会执行 dep.notify
,然后 Computed Watcher 就执行 update
方法(可见上方代码), dirty=true
。从而跟第一次渲染一样,重新获取 Computed Watcher 的值。
但是这有一个问题,因为 Data 的 Dep 只收集了 Computed Watcher,并不会收集 Render Watcher,如果视图中没有引用 Data 的属性,只引用 Computed 的属性,当 Data 的值被修改后,只会触发 Computed Watcher,而不会触发渲染 Watcher。
所以在 createComputedGetter
方法里有一段代码:
if (Dep.target) {
watcher.depend();
}
上面说到维护 Watcher 是一个栈,当 Computed Watcher 获取完最新的值之后,Computed Watcher 就被弹出,这时候栈里仅剩 Render Watcher。 Computed Watcher 去执行 depend 的时候,就会把其所收集的 data dep,也一并让 Render Watcher 也一并收集。
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
看看这个例子的 Watcher 与 Dep 的关系:
<template>
{{ b }}
</template>
<script>
export default {
data() {
return {
a: 1,
}
},
computed: {
b() {
return this.a;
}
}
}
</script>
如果视图也引用了 a 属性, Render Watcher 会不会重新添加?
答案是不会的,因为 Watcher 去收集 Dep 的时候,会通过 Dep Set 去判断 id 是否已经被收集过,不会出现重复收集的情况。
# User Watcher
User Watcher 是所有 Watcher 类型最简单的一种,它的逻辑跟 Render Watcher 类似,只是目的不一样,一个是执行渲染,一个是执行用户自己定义的函数。
先看看 Vue 对 watch 属性的处理:
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key];
if (isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
function createWatcher(
vm: Component,
expOrFn: string | (() => any),
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options);
}
Vue.prototype.$watch = function (
expOrFn: string | (() => any),
cb: any,
options?: Record<string, any>
): Function {
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) {
const info = `callback for immediate watcher "${watcher.expression}"`;
pushTarget();
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
popTarget();
}
return function unwatchFn() {
watcher.teardown();
};
};
在处理 watch 属性的时候,就会获取其所监听的 data
属性或者 computed
属性,以及用户自己定义的回调 handler
,并为其创建一个 Watcher。
对于 User Watcher, 基于公共属性,添加了少许属性
{
user: boolean; // 用户Watcher
cb: Function; // 用户定义的回调
}
当用户修改 data 的属性时,跟 Render Watcher 一样,执行 dep.notify → update → run
。User Watcher 会获取 data 上属性的最新值,和原本存储在 value 的旧值作比对,如果不一致,就执行用户的回调 handler
(在 User Watcher 上的 cb
)。
如果是监听 Computed 属性呢?
其实就是将上图的 Render Watcher 换成 User Watcher,执行逻辑和顺序都是一样的。
# 五、总结
总的来说,Watcher 和 Dep 是 Vue2 响应式核心,也是 Vue2 的核心设计,理解三种 Watcher 的设计有助于更好的使用 Vue。在 Vue3 中,会将 Watcher 进行重构,这块留到介绍 Vue3 的时候再讲解。