Vue3 的响应式系统源码级 track 与 trigger 的依赖收集与触发更新详细流程解析
字数 1458 2025-11-20 01:01:16
Vue3 的响应式系统源码级 track 与 trigger 的依赖收集与触发更新详细流程解析
1. 核心概念:依赖收集与触发更新的基本逻辑
Vue3 的响应式系统基于 Proxy 和 Effect 实现,核心流程分为两步:
- track(依赖收集):在读取响应式数据时,将当前正在执行的副作用函数(effect)收集到依赖集合中。
- trigger(触发更新):在修改响应式数据时,从依赖集合中取出所有关联的 effect 并重新执行,驱动视图更新。
2. 依赖收集(track)的详细步骤
步骤 1:建立响应式数据与 effect 的映射关系
Vue3 使用一个全局的 targetMap(类型为 WeakMap)存储所有响应式对象的依赖关系:
targetMap的键是原始对象(target),值是一个depsMap(类型为Map)。depsMap的键是对象的属性名(key),值是一个dep(类型为Set),存储所有依赖该属性的 effect。
步骤 2:触发 track 的时机
当在 effect 中读取响应式数据时(例如 obj.foo),Proxy 的 get 拦截器会调用 track 函数:
// 简化版 track 逻辑
function track(target, key) {
if (!activeEffect) return; // 无活跃 effect 则直接返回
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect); // 将当前 effect 添加到依赖集合
activeEffect.deps.push(dep); // 同时记录 effect 对应的 dep(用于清理)
}
关键点:
activeEffect是全局变量,指向当前正在执行的副作用函数(通过effect函数注册)。- 每个 effect 会记录自己所有的依赖集合(
deps),用于后续清理(避免遗留依赖)。
3. 触发更新(trigger)的详细步骤
步骤 1:修改数据时触发 Proxy 的 set 拦截器
当对响应式数据赋值(例如 obj.foo = 1)时,会调用 trigger 函数:
// 简化版 trigger 逻辑
function trigger(target, key, type) { // type 表示操作类型(SET、ADD、DELETE)
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = new Set();
// 收集需要执行的 effect
if (key !== undefined) {
const dep = depsMap.get(key);
dep && dep.forEach(effect => effects.add(effect));
}
// 处理数组的 length 变更或新增元素等特殊情况
if (type === 'ADD' && Array.isArray(target)) {
const lengthDep = depsMap.get('length');
lengthDep && lengthDep.forEach(effect => effects.add(effect));
}
// 执行所有关联的 effect
effects.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect); // 支持调度器(如批量更新)
} else {
effect(); // 直接执行
}
});
}
关键点:
- 通过
target和key从targetMap中找到对应的dep(依赖集合)。 - 为避免无限循环(effect 内修改依赖数据),Vue3 使用调度器(scheduler)控制执行时机(例如放入微任务队列)。
4. 依赖清理与重新收集的机制
问题:为什么需要清理依赖?
考虑以下场景:
const obj = reactive({ flag: true, a: 1, b: 2 });
effect(() => {
console.log(obj.flag ? obj.a : obj.b);
});
// 首次执行:依赖 obj.flag 和 obj.a
obj.flag = false;
// 第二次执行:应依赖 obj.flag 和 obj.b,但旧依赖(obj.a)需清理
解决方案:每次执行 effect 前清理旧依赖
在 effect 执行前,会先从其 deps 中移除所有关联的依赖集合,然后重新收集:
function cleanup(effect) {
for (const dep of effect.deps) {
dep.delete(effect); // 从所有依赖集合中移除该 effect
}
effect.deps.length = 0; // 清空 effect 的依赖记录
}
执行流程:
- 触发
obj.flag = false→ 执行 effect。 - 执行前调用
cleanup清除旧依赖(obj.a的 dep 移除该 effect)。 - 执行 effect 函数,读取
obj.flag和obj.b→ 重新收集依赖。
5. 总结:track 与 trigger 的协同流程
- 初始化:
- 通过
reactive()创建 Proxy 代理对象。 - 通过
effect()注册副作用函数,执行时设置activeEffect为当前 effect。
- 通过
- 依赖收集:
- 读取响应式数据 → Proxy 的
get调用track→ 将 effect 存入对应dep。
- 读取响应式数据 → Proxy 的
- 触发更新:
- 修改响应式数据 → Proxy 的
set调用trigger→ 从dep中取出 effect 执行。
- 修改响应式数据 → Proxy 的
- 依赖更新:
- effect 执行前清理旧依赖,执行时重新收集新依赖。
通过这一机制,Vue3 实现了精确的依赖跟踪和高效的更新触发,避免不必要的重渲染,提升性能。