Skip to content

1.订阅-发布

Dep 类是 vue2 中用于管理依赖的类。

该类主要利用订阅-发布模式实现了管理依赖的添加、删除和通知更新。

其中,Dep 类中维护了一个 subs 数组,用于存储依赖。

  1. addSub 方法用于添加依赖;
  2. removeSub 方法用于删除依赖;
  3. notify 方法用于通知依赖更新。
  4. depend 方法用于将依赖实例添加到 Watcher 中(Dep.target 对应的是 Watcher 实例)。

其中,addSubnotify 逻辑较清晰,无需其他说明。

下一节,我们重点分析 removeSubdepend 方法。

ts
class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget | null>
  // pending subs cleanup
  _pending = false

  constructor() {
    this.id = uid++
    this.subs = []
  }

  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }

  removeSub(sub: DepTarget) {
    // #12696 deps with massive amount of subscribers are extremely slow to
    // clean up in Chromium
    // to workaround this, we unset the sub for now, and clear them on
    // next scheduler flush.
    this.subs[this.subs.indexOf(sub)] = null
    if (!this._pending) {
      this._pending = true
      pendingCleanupDeps.push(this)
    }
  }

  depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
      Dep.target.addDep(this)
      if (__DEV__ && info && Dep.target.onTrack) {
        Dep.target.onTrack({
          effect: Dep.target,
          ...info
        })
      }
    }
  }

  notify(info?: DebuggerEventExtraInfo) {
    // stabilize the subscriber list first
    const subs = this.subs.filter(s => s) as DepTarget[]
    if (__DEV__ && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      const sub = subs[i]
      if (__DEV__ && info) {
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })
      }
      sub.update()
    }
  }
}

2. removeSub

removeSub 方法用于删除依赖。

针对性解决了 Chrome 浏览器中大量订阅者清理时的性能问题。

  1. Chrome 中,当一个依赖(Dep)有大量订阅者(subscribers)时
  2. 直接使用 Array.splice()filter() 来移除订阅者会非常慢
  3. 这是 Chrome 浏览器的一个特定问题(issue #12696

解决方案

typescript
removeSub(sub: DepTarget) {
  // 不直接从数组中移除元素,而是先将其设置为 null
  this.subs[this.subs.indexOf(sub)] = null
  // 如果还没有待清理标记
  if (!this._pending) {
    // 标记为待清理
    this._pending = true
    // 将这个 dep 添加到待清理队列
    pendingCleanupDeps.push(this)
  }
}

// 在下一个调度周期进行实际的清理
const cleanupDeps = () => {
  for (let i = 0; i < pendingCleanupDeps.length; i++) {
    const dep = pendingCleanupDeps[i]
    // 过滤掉所有 null 值
    dep.subs = dep.subs.filter(s => s)
    dep._pending = false
  }
  // 清空待清理队列
  pendingCleanupDeps.length = 0
}

假设有一个 dep 实例:

typescript
const dep = new Dep()

// 添加一些订阅者
const sub1 = { /* ... */ }
const sub2 = { /* ... */ }
dep.addSub(sub1)
dep.addSub(sub2)

// 移除订阅者
dep.removeSub(sub1)
// 此时 sub1 在数组中变成了 null,但还没有被真正移除
// [null, sub2]

// 在下一个调度周期
cleanupDeps()
// 此时才真正清理掉 null 值
// [sub2]

这种优化方案:

  1. 避免了频繁的数组操作
  2. 将清理操作批量化处理
  3. 显著提升了在 Chrome 中处理大量订阅者时的性能

3. depend

depend 方法用于将依赖实例添加到 Watcher 中(Dep.target 对应的是 Watcher 实例)。

ts
depend(info?: DebuggerEventExtraInfo) {
  if (Dep.target) {
    Dep.target.addDep(this)
    if (__DEV__ && info && Dep.target.onTrack) {
      Dep.target.onTrack({
        effect: Dep.target,
        ...info
      })
    }
  }
}

可以发现,核心作用是 Dep.target.addDep 方法。

Dep.target 的值,是通过 pushTargetpopTarget 方法设置的。

关于 pushTargetpopTarget 方法的逻辑:

ts
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]
}

设计 pushTargetpopTarget 主要是为了处理嵌套的 watcher 场景。

javascript
const vm = {
  computed: {
    // 计算属性 A
    computedA() {
      // 这里访问了计算属性 B
      return this.computedB + ' A'
    },
    // 计算属性 B
    computedB() {
      return this.message + ' B'
    }
  },
  data: {
    message: 'Hello'
  }
}

如果直接操作 Dep.target,会导致 watcher 被覆盖。

ts
class Watcher {
  get() {
    Dep.target = this       // 直接设置
    // 执行 getter
    const value = this.getter.call(vm, vm)
    Dep.target = null       // 直接清除
    return value
  }
}

// 问题展示
computedA 的 watcher 执行:
Dep.target = watcherA

执行 computedA 的 getter

访问 computedB,触发 computedB 的 watcher

Dep.target = watcherB          // ⚠️ watcherA 被覆盖了!

执行完 computedB

Dep.target = null              // ⚠️ watcherA 永远无法恢复!

使用 pushTargetpopTarget 的话:

ts
// 现在的执行流程
computedA 的 watcher 执行:
pushTarget(watcherA)           // targetStack = [watcherA]

执行 computedA 的 getter

访问 computedB,触发 computedB 的 watcher

pushTarget(watcherB)           // targetStack = [watcherA, watcherB]

执行完 computedB

popTarget()                    // targetStack = [watcherA],恢复 watcherA

继续执行 computedA

popTarget()                    // targetStack = []