依赖收集与触发机制深度解析
Vue3 响应式的发动机是 track 收集依赖与 trigger 触发更新,用 WeakMap 三层结构实现 O(1) 定位。 get 时记账、set 时讨账、effect 重跑前清旧账,理解这套三层数据结构就理解了整个框架高效更新的数学保证。
一句话概括
Vue 3 响应式的发动机是 track(依赖收集)和 trigger(触发更新),用 WeakMap<target, Map<key, Set<effect>>> 三层结构实现 O(1) 定位。get 时记账、set 时讨账、effect 重跑前清旧账——这是整个框架能 60fps 运行的数学保证。
核心知识点
1. 三层数据结构——O(1) 的依赖地图
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
11
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return; // 没有正在运行的 effect,不收集
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); // 🔑 反向引用——用于 cleanup
}
为什么 WeakMap? 被代理的对象销毁时,对应的依赖关系自动被 GC 回收。Map 做不到——它的 key 是强引用,阻止回收。
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; // ② 设为当前 effect
fn(); // ③ 执行 fn,内部的 get 自动 track
activeEffect = null; // ④ 复原
};
_effect.deps = []; // 反向引用数组
_effect(); // 立即执行收集第一轮依赖
return _effect;
}
为什么每次重跑前要 cleanup? 分支切换场景:effect(() => state.flag ? state.a : state.b)。flag 从 true 切 false 后,旧依赖 dep(a) 必须从这个 effect 中移除——否则改 state.a 仍然会触发这个不再关心 a 的 effect。
3. track 与 trigger 的配合——记账与讨账
1
2
3
4
5
6
7
8
9
// 每次 get → track(记账)
// 每次 set → trigger(讨账)
const state = reactive({ count: 0 });
effect(() => {
document.body.textContent = state.count; // get → track(state, 'count')
});
state.count++; // set → trigger(state, 'count') → effect 重跑
4. 嵌套 effect 的正确恢复——parent 指针
1
2
3
4
5
6
7
8
9
10
11
// 外层 effect 套内层 effect
effect(() => {
// activeEffect = effectOuter
console.log(state.a); // track → effectOuter
effect(() => {
// activeEffect = effectInner
console.log(state.b); // track → effectInner
});
// ⚠️ activeEffect 必须恢复为 effectOuter!
console.log(state.c); // 这里的 track 应该用 effectOuter
});
Vue 3 用 parent 指针代替数组栈处理嵌套——每次新 effect 执行前把当前的 activeEffect 存为 parent,执行完恢复。这个设计比数组栈更高效(大多数场景嵌套 ≤ 3)。
5. scheduler——trigger 不直接调用 effect
1
2
3
4
5
6
7
// effect 的第二个参数
effect(fn, {
scheduler(effect) {
// 不直接 run,而是推入微任务队列
queueMicrotask(() => effect());
}
});
这是异步批量更新的根: 渲染 effect 的 scheduler 走的是异步队列——同一 tick 内 100 次数据修改,渲染 effect 只被推入队列一次。所以用户永远看不到中间态。
其实你每天都在用
- 模板编译: 组件
render函数就是一个 effect,模板里访问的每个响应式变量自动被 track - computed 的懒计算: trigger 发现 computed 的依赖变了,只设
dirty = true,不立即算值 - watch 的 flush 控制: trigger 看到 effect 有 scheduler 就委托给它——
flush: 'post'的 watch 在 DOM 更新后才执行 - 组件卸载清理:
onUnmounted调用stop(effect)→ cleanup 清空所有 dep 引用 → 闭包引用全部断开 - v-if 分支切换: 条件从 true 切 false,cleanup 剪断旧分支的所有依赖链
常见误解(FAQ)
❌ 误区:「trigger 会立刻执行所有相关的 effect」 不会。trigger 先检查 effect 有没有
scheduler——computed 的 scheduler 只设 dirty,渲染 effect 的 scheduler 推入异步队列。只有普通effect(fn)才同步 run。❌ 误区:「effect 里修改依赖自己的数据没问题」
effect(() => { state.count++ })会死循环。虽然 Vue 3 有activeEffect比对拦截(trigger 不执行自己),但如果 effect A 改 B 又触发 A,链条长了仍可能跑飞。❌ 误区:「WeakMap 自动回收就万事大吉了」 WeakMap 的 key 是原始对象(不是 Proxy 包装后的),如果原始对象被全局变量引用,条目不会回收。而且
effect.deps的 Set 引用是强引用——必须调stop()或组件卸载来切断。❌ 误区:「每次 get 都 track」 有
shouldTrack全局标志位,Vue 内部在某些场景(如遍历数组、执行 computed getter 的部分阶段)会pauseTracking(),避免收集到不该收集的依赖。
一句话总结
track 在 get 里记账(谁在用我),trigger 在 set 里讨账(通知所有债主),cleanup 在重跑前清旧账(剪断不再需要的依赖链)——Vue 响应式的核心不是 Proxy,而是这套 O(1) 的依赖会计系统。