Vue3 响应式原理拆解:从 Proxy 代理到依赖收集的完整链路

发布时间:2026/6/14 20:53:55
Vue3 响应式原理拆解:从 Proxy 代理到依赖收集的完整链路 Vue3 响应式原理拆解从 Proxy 代理到依赖收集的完整链路一、Vue2 响应式的局限Object.defineProperty 的三个盲区Vue2 使用 Object.defineProperty 实现响应式但这个 API 有三个无法绕过的局限第一无法检测属性的新增和删除obj.newProp value不触发更新第二无法检测数组索引的直接赋值arr[0] newVal不触发更新第三深度监听需要在初始化时递归遍历所有属性对大型对象的初始化性能影响显著。Vue3 用 Proxy 替代 Object.defineProperty从根本上解决了这三个问题。Proxy 可以拦截对象的所有操作包括属性新增、删除和数组索引修改且深度监听是惰性的——只有被访问的属性才会被代理未访问的属性不会触发任何开销。二、Vue3 响应式系统的核心机制Vue3 响应式系统的核心是三个概念的协作响应式代理Proxy、依赖收集Effect和调度更新Scheduler。flowchart TB READ[读取属性 obj.name] -- TRACK[依赖收集 track] TRACK -- EFF[记录当前 Effect] EFF -- MAP[targetMap] WRITE[写入属性 obj.name new] -- TRIGGER[触发更新 trigger] TRIGGER -- MAP MAP -- RUN[执行相关 Effects] RUN -- SCHED[Scheduler 调度] SCHED -- FLUSH[批量刷新 DOM] subgraph 响应式代理 Proxy READ WRITE end subgraph 依赖收集 TRACK EFF MAP[targetMap WeakMap] end subgraph 调度更新 TRIGGER RUN SCHED FLUSH endProxy 拦截通过get拦截器实现依赖收集谁读取了这个属性通过set拦截器实现触发更新通知所有依赖这个属性的 Effect 重新执行。依赖收集使用全局的targetMapWeakMap存储依赖关系。结构是targetMap → target → Map(key → Set(effect))。当 Effect 执行时读取某个属性这个 Effect 就被记录到该属性的依赖集合中。调度更新多个属性同时变更时对应的 Effect 不应该立即执行而是被收集到队列中在下一个微任务中批量执行。这避免了同一个 Effect 在一次事件循环中被多次执行。三、Vue3 响应式核心的简化实现/** * Vue3 响应式系统简化实现 * 核心三件套reactive / effect / computed */ // --- 全局状态 --- let activeEffect: ReactiveEffect | null null; const targetMap new WeakMapobject, Mapstring | symbol, SetReactiveEffect(); // --- Effect --- class ReactiveEffect { private _fn: () void; private _scheduler?: () void; deps: SetReactiveEffect[] []; // 该 Effect 被哪些依赖集合引用 constructor(fn: () void, scheduler?: () void) { this._fn fn; this._scheduler scheduler; } run() { // 设置全局 activeEffect使依赖收集能获取当前 Effect activeEffect this; try { return this._fn(); } finally { // 执行完毕后清除避免后续无关操作误收集 activeEffect null; } } stop() { // 从所有依赖集合中移除该 Effect使其不再被触发 for (const dep of this.deps) { dep.delete(this); } } } // --- 依赖收集与触发 --- function track(target: object, key: string | symbol) { if (!activeEffect) return; // 不在 Effect 上下文中无需收集 let targetDeps targetMap.get(target); if (!targetDeps) { targetDeps new Map(); targetMap.set(target, targetDeps); } let keyDeps targetDeps.get(key); if (!keyDeps) { keyDeps new Set(); targetDeps.set(key, keyDeps); } if (!keyDeps.has(activeEffect)) { keyDeps.add(activeEffect); // 反向引用Effect 记录自己被哪些集合引用便于 stop 时清理 activeEffect.deps.push(keyDeps); } } function trigger(target: object, key: string | symbol) { const targetDeps targetMap.get(target); if (!targetDeps) return; const keyDeps targetDeps.get(key); if (!keyDeps) return; // 创建副本遍历避免 Effect 执行过程中修改集合 const effectsToRun new Set(keyDeps); for (const effect of effectsToRun) { // 有 scheduler 则走调度异步批量否则同步执行 if (effect._scheduler) { effect._scheduler(); } else { effect.run(); } } } // --- reactive --- const reactiveMap new WeakMapobject, any(); function reactiveT extends object(target: T): T { if (reactiveMap.has(target)) return reactiveMap.get(target); const proxy new Proxy(target, { get(target, key, receiver) { // 依赖收集谁读取了这个属性 track(target, key); const result Reflect.get(target, key, receiver); // 惰性深度代理只有被访问的嵌套对象才会被代理 if (result ! null typeof result object) { return reactive(result); } return result; }, set(target, key, value, receiver) { const oldValue Reflect.get(target, key, receiver); const result Reflect.set(target, key, value, receiver); // 只在值真正变化时触发更新避免无意义渲染 if (oldValue ! value) { trigger(target, key); } return result; }, deleteProperty(target, key) { const hadKey Reflect.has(target, key); const result Reflect.deleteProperty(target, key); if (hadKey result) { trigger(target, key); // 属性删除也触发更新 } return result; }, }); reactiveMap.set(target, proxy); return proxy; } // --- effect --- function effect(fn: () void, options?: { scheduler?: () void }) { const _effect new ReactiveEffect(fn, options?.scheduler); // 立即执行一次触发依赖收集 _effect.run(); // 返回 runner允许手动触发 const runner _effect.run.bind(_effect); (runner as any).effect _effect; return runner; } // --- computed --- function computedT(getter: () T) { let value: T; let dirty true; // 脏标记是否需要重新计算 const runner effect(getter, { scheduler: () { // getter 的依赖变化时标记为脏但不立即计算 dirty true; // 触发依赖 computed 值的 Effect 重新执行 trigger(computedObj, value); }, }); const computedObj { get value() { if (dirty) { value runner(); dirty false; // 计算后清除脏标记 } track(computedObj, value); // computed 自身也可被依赖 return value; }, }; return computedObj; }四、Vue3 响应式的 Trade-offs 分析Proxy 的性能开销Proxy 的 get/set 拦截比直接属性访问慢约 5-10 倍。对于高频访问的热路径如循环中的数组元素这个开销会累积。Vue3 通过缓存和惰性代理缓解了大部分问题但极端场景下仍需注意。深度响应式的内存占用每个被代理的对象都会在 targetMap 中创建依赖记录。对于包含大量嵌套对象的数据结构内存占用可能显著增加。如果某些嵌套对象不需要响应式可以使用markRaw标记跳过代理。Effect 的隐式依赖依赖收集在 Effect 执行时自动发生开发者无法直观看到这个 Effect 依赖了哪些属性。当 Effect 意外触发时排查依赖关系需要借助开发工具。Vue DevTools 提供了依赖追踪功能但在复杂场景下仍然不够直观。computed 的缓存陷阱computed 只在依赖变化时重新计算但如果 getter 中有副作用如发起 API 请求副作用不会在缓存命中时执行。computed 应该是纯函数副作用应该放在 watch 中。五、总结Vue3 响应式系统用 Proxy 替代 Object.defineProperty解决了属性新增/删除检测和深度监听性能问题。核心机制是Proxy 拦截 → 依赖收集 → 调度更新三步链路。惰性深度代理确保只有被访问的属性才产生开销批量调度避免同一 Effect 重复执行。落地时需要注意 Proxy 的性能开销、深度响应式的内存占用和 computed 的缓存语义。建议用shallowReactive处理大型对象用markRaw跳过不需要响应式的部分。