文章

依赖收集与触发机制

依赖收集与触发机制

一句话概括

Vue 3 响应式的核心引擎是依赖收集(track)与触发更新(trigger)——在数据读取时”记住谁在用我”,在数据修改时”通知所有依赖者”,通过 WeakMap → Map → Set 三层数据结构实现 O(1) 级查找。

核心知识点

1. targetMap——中央仓库的数据结构

1
2
3
WeakMap<target, Map<key, Set<effect>>>
  target1 → { count → [effectA, effectB], name → [effectC] }
  target2 → { items → [effectD] }
1
2
3
4
5
6
7
8
9
10
const targetMap = new WeakMap();

function track(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let deps = depsMap.get(key);
  if (!deps) depsMap.set(key, (deps = new Set()));
  deps.add(activeEffect);
  activeEffect.deps.push(deps); // 反向引用,用于清理
}

为什么用 WeakMap? 当响应式对象被销毁时,对应的依赖关系自动被 GC 回收,不会内存泄漏。

2. effect——副作用的载体

1
2
3
4
5
6
7
8
9
10
11
12
13
let activeEffect = null;

function effect(fn) {
  const _effect = () => {
    cleanup(_effect);      // 先清旧依赖
    activeEffect = _effect;
    fn();                  // 执行时自动 track
    activeEffect = null;
  };
  _effect.deps = [];
  _effect();               // 立即执行收集依赖
  return _effect;
}

3. track 与 trigger 的配合

1
2
3
4
5
6
7
8
9
// 每次 get 时 —— track
// 每次 set 时 —— trigger
const state = reactive({ count: 0, name: 'Vue' });

effect(() => {
  console.log(state.count);  // 读取 → track(state, 'count')
});

state.count++;  // 修改 → trigger(state, 'count') → 重新执行 effect

4. 依赖清理与分支切换

1
2
3
4
5
6
7
8
9
10
11
12
// 每轮 effect 重跑前,必须清理旧依赖
function cleanup(effect) {
  effect.deps.forEach(dep => dep.delete(effect));
  effect.deps.length = 0;
}

// 分支切换场景
effect(() => {
  const val = state.flag ? state.a : state.b; // flag 变化时
  // 旧依赖 dep(a) 需要从 effect 中移除
  // 新依赖 dep(b) 需要加入 effect
});

不清理的后果flag 从 true 切到 false 后,修改 state.a 仍然触发这个 effect——完全不该更新的更新了。

5. 嵌套 effect 与 activeEffect 栈

1
2
3
4
5
6
7
8
9
10
effect(() => {
  // activeEffect = effectOuter
  console.log(state.outer);
  effect(() => {
    // activeEffect = effectInner
    console.log(state.inner);
  });
  // activeEffect 必须恢复为 effectOuter!
  console.log(state.other); // 这里的 track 应该用 effectOuter
});

Vue 3 用 parent 指针替代数组栈(大多数场景嵌套深度 ≤ 3),更高效。

其实你每天都在用

  • 模板编译:组件的 render 函数就是一个 effect,模板里访问过的每个响应式变量都自动被 track
  • computed 懒计算:只有依赖变化 + 自己被读取时才重新计算——背后是 trigger 只设 dirty,不立即求值
  • watch 的回调调度:trigger 发现 effect 有 scheduler 就调 scheduler,而不是直接 run——这就是 watch flush 控制的底层
  • v-if 条件渲染:条件从 true 切到 false,依赖清理机制确保旧的依赖链被剪断,不会无谓更新
  • 组件卸载onUnmountedstop(effect) 触发 cleanup,将所有 dep 中的引用清空

常见误解

  • ❌ 误区:「trigger 会立即执行所有 effect」 不会。trigger 收集所有相关 effect 后,如果 effect 有 scheduler(如 computed 的 dirty-setter、watch 的 flush 调度器),就委托给 scheduler,只有普通 effect 才直接 run。这也是同一帧内多次修改只触发一次渲染的原理——渲染 effect 的 scheduler 走的是异步队列。

  • ❌ 误区:「effect(() => { state.count++ }) 是合法的」死循环!effect 里修改依赖自己的数据 → 触发 trigger → 重新执行 effect → 又修改……虽然 Vue 3 有 activeEffect 比对来拦截(trigger 不会重新执行自己),但如果 effect A 改 B 又触发 A,链条够长仍然可能形成循环。

  • ❌ 误区:「WeakMap 的 key 是 Proxy 对象,垃圾回收后就安全了」 对,但前提是原始对象本身也不被引用。如果原始对象被全局变量持续引用,WeakMap 条目不会被回收。另外,effect.deps 的反向引用是强引用——必须调用 stop() 或组件卸载时的 cleanup 才能切断。

  • ❌ 误区:「依赖收集发生在每个 get 调用」 不是。有一个全局 shouldTrack 标志位,在某些场景(如 pauseTracking())会被关闭。Vue 内部在遍历数组、执行 computed getter 时部分场景也会暂停追踪,避免收集到不该收集的依赖。

一句话总结

track 在 get 里记账,trigger 在 set 里讨账,WeakMap 兜底防内存泄漏——这就是 Vue 3 响应式的”资产负债表”。

本文由作者按照 CC BY 4.0 进行授权