响应式系统(三)
上一节 响应式系统(二) 中大致说明了 Observer 与 observe 通过 Object.defineProperty 实现数据响应式处理的过程,该过程也常称为 “数据劫持”。那么在数据的更新与读取过程都被劫持之后,就该处理 dom 与数据的依赖关系了,所以这一节我们简单学习一下 Dep 依赖收集。
1. Dep
Dep,应该就是 dependence 的简写,表示依赖关系;并且肯定具有两个属性:依赖订阅者(观察者 watcher)数组与被依赖者。
Vue 2 中的 Dep 构造函数定义如下:
export interface DepTarget { id: number addDep(dep: Dep): void update(): void } export default class Dep { static target?: DepTarget | null id: number subs: Array<DepTarget> constructor() { this.id = uid++ this.subs = [] } addSub(sub: DepTarget) { this.subs.push(sub) } removeSub(sub: DepTarget) { remove(this.subs, sub) } depend() { if (Dep.target) { Dep.target.addDep(this) } } notify() { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
上面省略了开发环境中的 debug 处理部分;并且在开发环境下还可以在 notify 中选择同步按顺序执行。
整个 Dep 类的定义比较简单:
- 接收一个类静态属性 target,用来标记后面的订阅者依赖的数据。并且这里采用的是静态属性,而非每个 Dep 实例独有的属性,也是为了保证当前依赖订阅者在处理时不会被其他方式改变造成异常;并且该 target 对象必须包含两个方法:
- addDep:将当前依赖添加到订阅者的依赖数组中
- update:数据改变时触发的回调函数
- addSub 与 removeSub:用来管理该依赖的订阅者数组
- depend:在定义了 Dep.target 之后,将该依赖添加到依赖订阅者的依赖数组中
- notify:在当前数据改变时触发所有依赖订阅者的更新操作
当然,此时还需要一个修改 Dep.target 的方法
2. Dep.target
Dep.target = null const targetStack: Array<DepTarget | null | undefined> = [] export function pushTarget(target?: DepTarget | null) { targetStack.push(target) Dep.target = target } export function popTarget() { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
这里默认 Dep.target 是一个空值,并且定义了一个变量 targetStack,根据命名来判断,是用来保存历史的 Dep.target 并可以回退以前目标对象的栈。
📌个人理解这里为什么会用一个数组和两个方法来处理当前订阅对象 Dep.target ?
因为在处理生命周期钩子函数 callHook,初始化 data 数据,触发 watch 回调函数的时候,为了避免内部数据改变导致依赖对应的数据的依赖订阅者(观察者)重复更新,导致数据和回调函数重复更新与执行,所以在以上几个阶段都会停止依赖收集。
而通过 Stack 和两个方法,可以简单通过处理开始前调用 pushTarget() 来停止依赖收集,并在结束后调用 popTarget() 来恢复原有的依赖引用。
🚀🚀🚀 Dep 在设计上都依赖 依赖订阅者(观察者)来实现完整的依赖订阅逻辑,脱离 Watcher 之后 Dep 是无法正常完成逻辑执行的。