依赖收集与触发机制
一句话概括
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,依赖清理机制确保旧的依赖链被剪断,不会无谓更新
- 组件卸载:
onUnmounted中stop(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 响应式的”资产负债表”。