人人都会AI编程

4.3 依赖收集与派发更新全链路

更新时间:2026-07-09

响应式系统的核心只有两件事:知道谁在用这个数据(依赖收集),以及数据变了通知谁(派发更新)。Vue 3 和 Vue 2 的具体实现不同,但思想相通。下面以 Vue 3 为主讲解全链路,再简要对比 Vue 2。

一、Vue 3 的核心角色:effect、track、trigger

1. effect:副作用函数
任何一个依赖了响应式数据的函数,都可以被包装成一个 effect。比如组件的渲染函数、watch 的回调、computed 的 getter。effect 执行时会自动收集它触碰到的所有响应式数据。

effect(() => {
  document.body.innerText = state.count
})

2. track:依赖收集
effect 执行并读取了某个响应式属性(如 state.count)时,Vue 内部的 track 函数会被触发。它的任务是把“这个 effect”和“这个属性”绑定起来。

存储结构大致是这样的(简化版):

targetMap: WeakMap<源对象, Map<属性, Set<effect>>>
  • 第一层:用 WeakMap 以响应式对象为 key,确保对象被垃圾回收时对应依赖也消失。
  • 第二层:每个对象的每个属性(或键)对应一个 Map,key 是属性名。
  • 第三层:属性值对应一个 Set,里面存放所有依赖该属性的 effect

所以当 effect 读了 state.counttrack 就把它加入 targetMap.get(state).get('count') 这个 Set 里。

3. trigger:派发更新
state.count 被修改时,trigger 会沿着同样的路径找到 targetMap 里存的 effect 集合,然后执行(或重新调度)它们。因为响应式数据通过 Proxy 拦截了 set 操作,trigger 就是在 set 拦截中调用的。

整条链路:

effect 执行 → 读数据 → Proxy get 拦截 → track 收集依赖
↓
数据修改 → Proxy set 拦截 → trigger 找到依赖的 effect 集合 → 执行 effect

二、一个更具体的示例

const obj = reactive({ a: 1, b: 2 })

effect(() => {
  console.log(obj.a)
})
// 打印 1
// 此时 targetMap: { obj → { a → [ effect ] } }

obj.a = 10  // trigger 触发,effect 再次执行,打印 10
obj.b = 20  // 没有 effect 依赖 b,什么也不发生

组件渲染也是同样的道理。每个组件的渲染函数本身就是一个 effect。当组件模板里用到了 state.count,该渲染 effect 就被收集为 count 的依赖。count 一变,组件自动重渲染。

三、依赖清理与避免重复收集

一个 effect 可能被执行多次,每次执行时依赖的数据可能不同。比如:

effect(() => {
  if (state.flag) {
    console.log(state.a)
  } else {
    console.log(state.b)
  }
})
  • 第一次执行,flag === true,依赖了 flaga
  • flag 变为 false 重新执行,这次依赖了 flagb,不再依赖 a

如果不做清理,a 的依赖集合里仍然保留着这个 effect,以后 a 变化时还会触发它执行,但实际上这时 a 已经不再影响渲染结果,白白浪费性能。

Vue 3 解决了这个问题:每个 effect 都会记录自己当前依赖了哪些属性,在重新执行之前,会先清除自己与这些属性的绑定,然后执行时重新收集。这样就能保证依赖集合始终是最新的。

具体实现:

  • 每个 effect 上有一个 deps 数组,存放着所有它订阅过的属性对应的 Set(或 Dep)。
  • 每次 effect 执行前,先遍历 deps,把自己从这些 Set 中删除。
  • 执行过程中,track 又会把新的依赖加进来。
  • 这种“先清后收”的流程,确保了永远不会有多余的旧依赖残留

同时也避免了重复收集:同一个属性被多次读取时(比如模板里用了两次 state.count),track 内部会检查 Set 是否已经包含当前 effect,如果是就跳过,所以 Set 里每个 effect 只出现一次。

四、Vue 2 的对应机制:Dep 与 Watcher

Vue 2 的角色名字不同,但结构类似:

  • Dep 相当于一个属性的依赖收集容器(类似前面 Map 里的 Set)。
  • Watcher 相当于 effect。每个组件渲染、每个 watch、每个 computed 都对应一个 Watcher 实例。

Vue 2 也是通过 Object.defineProperty 的 getter 里调用 dep.depend() 收集 Watcher,setter 里调用 dep.notify() 通知所有 Watcher 更新。依赖清理也是通过 Watcher 自身的 deps 数组实现,机制一致。

五、为什么你需要理解这条链路?

  • 避免响应式失效:只有当数据在 effect 执行期间被同步读取,才会被收集。如果你在 setTimeout 或异步回调里读取数据,或者使用普通函数的返回值去引用,可能收集不上,导致数据变了视图不更新。
  • 理解 nextTick 的必要性:多个数据变更在同一个同步块内,Vue 会把 effect 加入微任务队列批量执行,避免多次不必要的计算和 DOM 操作。
  • 防止内存泄漏:如果手动创建 effect(如使用 watchEffect),在组件卸载时应当停止,否则 effect 会一直持有组件数据的引用,导致组件无法被垃圾回收。Vue 3 的 effectScope 可以帮助统一管理。
  • 优化 computedwatch 的性能:知道依赖是精确收集的,你就可以放心拆分计算属性和侦听器,而不用担心性能浪费。

简单总结:数据变了知道通知谁,是因为中间藏着一张精确的“依赖地图”;视图更新为什么高效,是因为这张地图只指向真正需要更新的地方。 这就是响应式系统最核心的“后勤保障”。