我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
响应式系统(四)
关于 Vue 2 响应式系统的前三节已经讲述了 “数据劫持 Observer” 与 “依赖收集分发 Dep”,但是关于 “依赖收集分发” 部分还有还有 “观察者 Watcher” 没有解析。
上文也提到了,Dep 不能脱离 Watcher 单独使用。
1. Watcher 定义
该类定义位于 src/core/observer/watcher.ts,定义如下:
export default class Watcher implements DepTarget {
vm?: Component | null
expression: string
cb: Function
id: number
deep: boolean
user: boolean
lazy: boolean
sync: boolean
dirty: boolean
active: boolean
deps: Array<Dep>
newDeps: Array<Dep>
depIds: SimpleSet
newDepIds: SimpleSet
before?: Function
onStop?: Function
noRecurse?: boolean
getter: Function
value: any
post: boolean
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
recordEffectScope(this, activeEffectScope || (vm ? vm._scope : undefined))
if ((this.vm = vm)) {
if (isRenderWatcher) {
vm._watcher = this
}
}
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid
this.active = true
this.post = false
this.dirty = this.lazy
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = ''
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
!this.getter && (this.getter = noop)
}
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: any) {
} finally {
this.deep && traverse(value)
popTarget()
this.cleanupDeps()
}
return value
}
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)
}
}
}
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp: any = 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
}
// ... 其他实例方法
}
2. 参数和属性分析
在 class Watcher 中定义了接近 20 个属性(排除了 dev 环境的 debug 支持),除了标识唯一性的 id 属性、当前组件实例的 vm 属性、观察变更时的回调 cb 之外,其他的属性大致可以进行以下分类:
- 观察对象相关属性,包括 expression,getter,value,deep,其中 expression 主要是用来进行开发模式下的错误提示,getter 用来获取当前时刻下 expOrFn 获取到的值,deep 则表示深度监听处理
- 观察对象改变时的回调函数执行控制,包括 deep,lazy,dirty,sync,post,active,before,user
- 其中 user 主要是用来处理回调函数执行时的错误信息处理,默认是 false,即不处理错误;
- before 则是一个可选的函数,会在 cb 回调函数执行前执行,这一点可以在 $mount 方法的定义中有明确体现(会在 before 中触发生命周期 beforeUpdate 的函数调用)
- active 用来控制是否可以触发,即是否执行 cb 回调
- dirty 则是控制 computed 计算属性的延迟更新,配合 lazy 一起实现;sync 与 post 用来确定 cb 执行时刻,是否是立即执行或者等待该次数据、试图更新结束之后再执行
- 依赖处理部分属性,包括 deps,newDeps,depIds,newDepIds 和 onStop,其中 onStop 用来处理该观察者依赖项被情况时执行的操作,其他几个属性则是用来收集当前状态解析/执行 expOrFn 时的依赖,并且针对性能做了一些优化处理(下面会讲)
3. 实例化与更新过程
3.1 属性初始化
在 new Watcher() 时,首先执行 recordEffectScope(),这个方法会将该 watcher 实例注册到 vm._scope.effects
数组中。
这里与 Vue 2.7 之前的版本有一些区别。在 Vue 2.6.14 及之前的版本中,组件内的 watcher 实例都是绑定在
vm._watchers
属性中的。
之后则是根据 options 参数进行属性初始化,这里有以下几点注意事项:
- depIds 与 newDepIds 采用的是 ES6 的 Set 结构,在后续 addDep 添加依赖时可以通过 depIds.has() 来去除重复依赖
- getter 会被定义成一个函数,就算是一个 xxx.xxx.xxx 结构的字符串,也会通过 parsePath 解析成一个函数
- 非 lazy 情况(非 computed 或者特殊声明)下,会执行 watcher.get() 来初始化当前时刻监听到的 expOrFn 的值
3.2 watcher.get() 收集依赖
在此过程中,首先就是通过 pushTarget(this) 将 Dep.target 指向当前的 watcher 实例,保证在 watcher.get 的过程中收集到的依赖都是对应到该 watcher 实例的。
之后则是执行 this.getter.call(vm, vm),获取到 watcher 实例的监听对象 expOrFn 此时对应的值;并且在执行过程中,会进行数据读取,触发 Object.defineProperty 中定义的 getter,调用 dep.depend() 执行依赖收集。上一节也已经讲过,dep.depend() 其实就是调用 Dep.target(也就是此时的 watcher 实例)的 addDep(this) 方法,将自己(该数据对应的 dep 实例)插入到 watcher 的 newDeps,newDepIds 中,并且只有在 原有的 watcher.deps 中不存在这个 dep 实例,才会在 dep.subs 中插入该 watcher,这样可以避免 watcher 重复。
如果配置了深度监听(deep 为 true)时,还会通过 traverse(value) 遍历获取结果触发每一级子属性的 getter 来进行依赖收集。
最后则是调用 popTarget() 结束当前阶段的依赖收集,并调用 cleanupDeps() 将 newDeps,newDepIds 的值复制给 deps, depIds 并清空。
当然,渲染 watcher(isRenderWatcher 为 true)此时对应的 expOrFn 为:
updateComponent = () => { vm._update(vm._render(), hydrating) }
这个模板解析过程中解析到的变量都会添加到 renderWatcher.deps 中
3.3 cleanupDeps() 清除依赖
整个 cleanupDeps() 函数的执行过程,就是清除无用依赖,然后将 newDeps, newDepIds 的值赋值给原始的 deps, depIds,并把 newDeps, newDepIds 清空,方便下次执行 get() 和 addDep() 的执行。
上面定义里有体现,在 addDep(dep) 时,会首先判断 newDepIds 中不存在这个数据的依赖引用 dep,并将这个 dep 添加到 newDepIds 和 newDeps 中;且当 depIds 中也不存在该 dep 时,才会执行 dep.addSub(),将 watcher 观察者添加到 dep 的通知对象 subs 中。
而 cleanupDeps() 中最先执行的 dep.removeSub(this),就是为了优化掉不需要被通知改变的一系列观察者。
最常见的场景就是使用 v-if,v-else-if,v-else 的场景,当节点对应的条件为 false 的时候,节点内部定义的一系列数据变化都是可以取消监听的,因为此时的节点都不会被渲染,也没有在 get() 中收集相关依赖,所以需要手动将原有的依赖从 dep.subs 中清理掉。
3.4 run() 与 update() 状态更新
在数据更新的时候,会通过 Observer - Object.defineProperty 定义的 setter 方法来处理,此时会触发 dep.notify()
而 dep.notify() 的逻辑也很简单,就是遍历 dep.subs 数组,依次执行 sub[i].update();上面的过程也可以看到,这里的 sub[i] 其实就是依赖这个数据的每一个 watcher 实例。
首先我们来看 update()
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这里也比较清晰,就是三个判断:
- lazy 为 true,表示延迟执行,此时把脏值标志(也就是 computed 的惰性更新机制)设置为 true
- 如果 sync 为 true,表示立即同步执行回调,则直接执行 run() 方法
- 其他情况则使用 queueWatcher 将该次回调插入到更新后的执行队列中,待当前的组件实例更新结束之后再依次执行(当然,最后也是通过 watcher.run() 执行回调方法 cb)
然后就是核心方法之一 run()
run() {
if (this.active) {
const value = this.get()
if (value !== this.value || isObject(value) || this.deep) {
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
}
当然这里我省略了错误处理那部分内容。
这个方法也就处理了两个地方:
- 更新当前 watcher 的 value
- 执行 cb 函数
4. computed 支持
在上面的过程中,提到了 computed 配置产生的 watcher 实例(computedWatcher),默认会将 lazy 设置为 true,那么此时在执行 watcher.update() 时是会直接退出的,怎么处理这个 lazy 和 dirty 呢?
所以 Watcher 又定义了另外两个方法:evaluate() 和 depend(),其定义如下:
evaluate() {
this.value = this.get()
this.dirty = false
}
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
evaluate:
在触发 computed.getter(即读取 computed 配置的计算属性时),才会通过该方法获取到 watcher 实例当前的值,并作为 getter 返回值
depend:
处理 computed.getter 方法中遇到的变量,并生成一个 computedWatcher 插入到所有变量对应的 dep 实例的 subs 数组中
这里的逻辑可能有点绕,笔者的理解和描述也可能有误差,希望大家多多包涵和指出错误😘
5. v3 语法支持
因为在 Vue 2.7 之后是适配了 Vue 3 的 setup 语法(组合式 api)的,而 Vue 3 中的 watchEffect 支持取消监听,所以 Watcher 又增加了一个取消依赖订阅的方法。
teardown:
teardown() {
if (this.vm && !this.vm._isBeingDestroyed) {
remove(this.vm._scope.effects, this)
}
if (this.active) {
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
if (this.onStop) {
this.onStop()
}
}
}
作用如下:
- 在实例还存在且没有被销毁时,在 vm._scope.effects 将该 watcher 实例移除掉
- 如果该 watcher 实例是活动的(也就是没有配置取消监听),会依次在 watcher.deps 中清除掉当前 watcher 的依赖,并将 active 设置为 false
- 如果有配置停止监听时的处理方法,还会执行 onStop()