【4-1】响应式原理 之 观察者模式

Vue2的响应式原理,网上的资料太多了。这篇主要是把脉络梳理清楚,并且明确一些核心的概念是怎么实现的。比如依赖收集和派发更新具体指的是什么,还有一些细节处理,才造就了 Vue的合理运作。

总之,响应式原理 = 数据劫持 + 观察者模式 = 依赖收集 + 派发更新 = defineReactive + 同时存在Dep.target

Dep:维护观察者队列

其实Dep类职责很简单:

  • 【1】维护dep.subs这个wathcer数组,即订阅者的数组
    • dep.addSub 和 dep.removeSub 作为针对数组 dep.subs 的工具方法,不会作为一个事务的起始。
  • 【2】dep.notify:遍历执行watcher.update
  • 额外:用 id 标识 dep实例,方便优化这个过程。

Dep.target:因为访问 this.data的事务有很多,所以Dep.target的作用在于标志是由哪一个 watcher 发起的访问data,这时候才有必要“依赖收集”。

  • Dep文件闭包作用域内,“全局唯一的 Dep.target” 是怎么维护的:
    • Dep.target 和 targetStack都是闭包变量。而pushTarget函数和popTarget函数都是用于同时更新该2者。
      • 也就是在 watcher.get 执行 watcher.getter的前后,会push和pop,致使Dep.target指向的是当前执行watcher.getter的watcher。
      • 而targetStack的作用:维护一个栈结构,并且方便恢复上一个 Dep.target。(因为可能会递归child走相同流程,或者渲染watcher依赖计算属性watcher等等)

至此,对于 src/core/observer/dep.js 的所有模块都介绍完了,足以看懂源码。

// #摘取自vue #基本能用:
let uid = 0;
export default class Dep {
  // Dep就3属性:
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub) {
    this.subs.push(sub)
  }
  removeSub (sub) {
    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()
    }
  }
}
// Dep.target 是全局唯一的观察者,因为在任何时候只有一个观察者被处理。
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher:观察者

作为观察者,职责就是【1】初始化的时候载入相关数据或者方法 【2】加入观察者队列 【3】等通知执行: watcher.update被通知调用,在未来执行 watcher.run。额外还有优化dep实例中的watcher队列。

初始化 和 watcher.get

就3种使用方式: (尤其是在判断区分expOrFn转成watcher.getter的时候注意一下)

  • 渲染 watcher:new Watcher(vm, updateComponent || noop, noop, options)
  • 计算属性 watcher:new Watcher(vm, computed[key] || noop, noop, options)
  • 侦听器 watcher:this.$watch('include', cb) ==> new Watcher(vm, 'include', cb, options)

剩下的实例方法

由 Watcher 的职责决定,其多数的实例方法都不会作为一个“事务”的起始。

watcher.addDep 和 watcher.update都是等dep实例来调用的。搞得这么复杂就是为了解耦。

  • 【解释 dep.depend 和 watcher.addDep】:dep.depend ==> Dep.target.addDep(this)。核心逻辑是dep.addSub(this/*即该watcher*/),这就是依赖收集。【记住这里绕了一圈,即还要绕去watcher那边搞点优化】
  • 【解释 dep.notify 和 watcher.update】:dep.notify ==> subs[i].update()。核心逻辑就是 在未来执行 watcher.run(),也就是调用一开始存在 watcher的cb和watcher.get

在整体雏形的基础上,需要针对实际场景来进行优化:

  • 优化dep实例中的watcher队列
    • 从源码角度看: 就是watcher中,[deps, depIds] vs [newDeps, newDepIds]watcher.cleanupDeps() 等等
    • 【1】以防止视图上已经不需要的watcher被触发
      • 【在watcher.get()的末尾触发】watcher.cleanupDeps():防止视图上已经不需要的watcher被触发:对比 newDeps,把deps中已经不需要的watcher从“dep实例的subs”中移除
      • (比如有一个 data msg,一开始v-if = true,能响应式,后来v-if = false,那么就没有必要响应式(收集依赖)这个 msg字段了,也就是没必要去订阅这个msg的变化)
    • 【2】在 watcher.addDep的时候,防止重复添加本 watcher 进dep实例中的队列
// #摘取自vue #基本能用:
export default class Watcher {
  constructor (
    vm: Component, // 组件实例对象
    expOrFn: string | Function, // 要观察的表达式,函数,或者字符串,只要能触发取值操作
    cb: Function, // 被观察者发生变化后的回调
    options?: ?Object, // 参数
    isRenderWatcher?: boolean // 是否是渲染函数的观察者
  ) {
    {
      /* 和 vm 互相保存对方引用: */
      this.vm = vm // Watcher有一个 vm 属性,表明它是属于哪个组件的
      if (isRenderWatcher) {
        vm._watcher = this
      }
      vm._watchers.push(this) // 给组件实例的_watchers属性添加观察者实例
    }

    // 根据 options 更新 this.deep this.user this.lazy this.sync
    // this.sync = !!options.sync // 同步执行

    // 优化 watcher 队列相关:
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()

    // 【略去兼容各种情况】设置 getter,最终this.getter系一个函数。
    this.getter = expOrFn
    // 【略去选择性调用】this.get():
    this.value = this.get()
  }
  get () { // 触发取值操作:调用 watcher.getter,同时更新 Dep.target
    pushTarget(this) // 给 Dep.target 赋值
    
    const vm = this.vm
    // 执行getter,即执行观察者表达式
    let value = this.getter.call(vm, vm) // 略掉try catch的逻辑

    // if (this.deep) { traverse(value) }// 如果要深度监测,再对 value 执行操作

    popTarget()
    this.cleanupDeps() // 防止视图上已经不需要的watcher被触发
    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) // 【核心逻辑】dep 添加订阅者
      }
    }
  }
  update () {
    if (this.sync) {
      this.run() // 若同步直接运行
    } else { // 否则加入异步队列等待执行,也是执行 watch.run
      queueWatcher(this)
    }
  }
}

依赖收集 和 派发更新

前面一直没有讲“依赖收集”和“派发更新”,因为这是人造的概念,若在其间加上只不过是增加理解负担。

在理解观察者模式后,其实“发布订阅模式”也是很类似的,只不过“发布订阅模式”更加强大一点,后者改由增设“调度中心”来收集订阅的回调函数,这样解耦使得发布方也能订阅。

而“依赖收集”和“派发更新”主要用于描述观察者模式:

  • dep.depend()dep.addSub(watcher)即为“依赖收集”;
  • dep.notify(),即把订阅的回调全都执行一遍,即为“派发更新”

总结

响应式原理 = 数据劫持 + 观察者模式 = 依赖收集 + 派发更新 = defineReactive + 同时存在Dep.target

剩余未解释的部分将于后面的文章中解释。

文章目录
  1. Dep:维护观察者队列
  2. Watcher:观察者
    1. 初始化 和 watcher.get
    2. 剩下的实例方法
  3. 依赖收集 和 派发更新
  4. 总结