JavaScript Proxy实现Vue3响应式系统的依赖收集
理解响应式系统的核心在于依赖收集。当数据变化时,我们需要知道“谁”在用这个数据,以便通知它们更新。Vue 3 利用 Proxy 实现了比 Vue 2 Object.defineProperty 更强大、更简洁的依赖收集机制。本文将从零开始,用代码一步步构建一个简化的核心依赖收集系统。
第一部分:为什么需要“依赖”以及它是什么
-
定义一个最简单的
App函数,它内部使用了响应式数据。function App() { // 假设 `obj` 是一个响应式对象 document.body.innerText = obj.message; }在这个函数执行时,它会读取
obj.message的值。这里的App函数,就是obj.message这个数据的一个“依赖”。当obj.message改变时,我们需要重新执行App。 -
明确依赖收集的目标。我们需要在
App函数执行并读取obj.message时,自动记录下:“App依赖了obj.message”。这样,当obj.message被修改时,我们就能找到所有记录过的依赖(比如App)并触发它们。
第二部分:设计响应式系统的核心结构
我们需要三个核心部分来完成这个目标:
- 响应式数据源 (
reactive):将一个普通对象转换为响应式对象。 - 当前正在执行的副作用函数 (
activeEffect):一个全局变量,用来存储“正在收集依赖的函数”。 - 依赖收集器 (
track) 与 依赖触发器 (trigger)。
-
创建一个全局变量,用于存放“当前正在执行的副作用函数”。
// 当前被激活的、正在收集依赖的副作用函数 let activeEffect = null; -
定义一个“副作用函数”注册器
effect。它的作用是执行一个函数,并在执行前将它设置为当前的activeEffect。function effect(fn) { // 将传入的 fn 标记为当前激活的副作用 activeEffect = fn; // 立即执行一次 fn,在这个过程中,fn 内部对响应式数据的访问会被拦截并收集依赖 fn(); // 执行完毕后,清除激活状态(为简化,实际 Vue 会更复杂) activeEffect = null; } -
设计依赖存储的数据结构。我们需要一个地方来存储
数据 -> 依赖的映射。WeakMap是理想选择,因为它以对象为键,且不会造成内存泄漏。// 使用 WeakMap 存储所有响应式对象的依赖关系 // 结构: targetMap: { target -> depsMap: { key -> deps: Set } } const targetMap = new WeakMap();
第三部分:使用 Proxy 实现依赖收集与触发
这是最关键的一步。我们将用 Proxy 包装原始对象,拦截它的 get 和 set 操作。
-
定义
track函数,用于在get拦截时收集依赖。function track(target, key) { // 1. 如果没有当前激活的副作用,无需收集(可能是在非副作用函数中访问) if (!activeEffect) return; // 2. 获取或初始化 target 对应的 depsMap let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } // 3. 获取或初始化 key 对应的 deps (一个 Set,用于存储副作用函数) let deps = depsMap.get(key); if (!deps) { depsMap.set(key, (deps = new Set())); } // 4. 将当前激活的副作用函数收集到 deps 中 deps.add(activeEffect); } -
定义
trigger函数,用于在set拦截时触发所有依赖更新。function trigger(target, key) { // 1. 找到 target 的依赖映射 const depsMap = targetMap.get(target); if (!depsMap) return; // 无依赖,直接返回 // 2. 找到 key 对应的依赖集合 const deps = depsMap.get(key); if (!deps) return; // 无依赖,直接返回 // 3. 遍历执行所有依赖的副作用函数 deps.forEach(effectFn => effectFn()); } -
实现
reactive函数,返回原始对象的Proxy代理。function reactive(target) { return new Proxy(target, { // 拦截属性读取操作 get(target, key, receiver) { const result = Reflect.get(target, key, receiver); // 依赖收集 track(target, key); return result; }, // 拦截属性设置操作 set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); // 触发依赖更新 trigger(target, key); return result; } }); }
第四部分:串联测试整个流程
现在,让我们把所有部分组装起来,并验证它是否工作。
-
创建一个响应式对象
state。const state = reactive({ message: 'Hello Vue 3!', count: 0 }); -
定义两个副作用函数,它们会读取
state的不同属性。// 副作用 A:依赖 message effect(() => { console.log('Message is:', state.message); document.getElementById('message').innerText = state.message; }); // 副作用 B:依赖 count effect(() => { console.log('Count is:', state.count); document.getElementById('count').innerText = state.count; });当上面两个
effect执行时,它们内部会访问state.message和state.count。Proxy的get拦截器会被触发,调用track函数,将对应的副作用函数(A 或 B)收集到state.message或state.count的依赖集合中。 -
修改
state的属性,观察依赖是否被正确触发。// 修改 message,只应触发副作用 A state.message = 'Hello Proxy!'; // 控制台输出:Message is: Hello Proxy! // 页面上 ID 为 `message` 的元素内容会更新 // 修改 count,只应触发副作用 B state.count = 1; // 控制台输出:Count is: 1 // 页面上 ID 为 `count` 的元素内容会更新
第五部分:理解“依赖收集”的精确含义
通过上述步骤,我们实现了精准的依赖收集。
-
回顾依赖收集发生在哪里。它发生在
Proxy的get拦截器中。只有当一个副作用函数(被effect包裹的函数)执行并读取某个响应式属性时,该属性才会把这个副作用记录为依赖。 -
对比直接赋值与依赖收集。如果我们在
effect函数之外访问state.message,比如console.log(state.message),此时activeEffect是null,track函数会直接返回,不会发生依赖收集。这保证了只有“真正用到数据”的代码才会被数据变化所通知。 -
理解依赖的存储位置。依赖被存储在
targetMap -> depsMap -> deps (Set)这个三层结构中。WeakMap的键是原始对象(target),所以当原始对象被垃圾回收时,其所有相关依赖也会被自动清除,避免了内存泄漏。
第六部分:处理嵌套对象与 computed
我们的简单实现已经具备了核心能力,但要达到 Vue 3 的健壮性,还需处理一些边界情况。
-
处理嵌套对象。如果
state有一个属性是对象,我们需要对它也进行响应式转换,这通常在get拦截器中实现。// 在 reactive 函数的 get 拦截器中,增加对返回值的递归包装 get(target, key, receiver) { const result = Reflect.get(target, key, receiver); track(target, key); // 如果 result 是对象,则递归将其转换为响应式 if (typeof result === 'object' && result !== null) { return reactive(result); } return result; }现在,
state.nested = { value: 1 }中的state.nested.value也具有了响应式能力。 -
实现计算属性 (
computed) 的基本原理。computed本质上是一个惰性的、带缓存的副作用。它会订阅其依赖,并在依赖变化时标记自己为“脏”,在被再次读取时才重新计算。function computed(getter) { let value; let dirty = true; // 标记是否需要重新计算 const effectFn = effect(() => { // 执行 getter,getter 内部访问响应式数据会触发 track,收集 computed 为依赖 value = getter(); dirty = false; // 计算完成后标记为干净 }, { lazy: true // 告诉 effect 不要立即执行(我们这里简化了,实际需要修改 effect 实现) }); return { get value() { if (dirty) { value = effectFn(); // 如果脏了,重新计算 } // 当读取 computed.value 时,也需要收集依赖,以便外部 effect 能订阅到 computed track(computedObj, 'value'); // 这里需要一个目标对象,通常 computed 本身会创建一个 return value; } }; }注意:这是一个高度简化的概念演示。Vue 3 中的
computed实现复杂得多,涉及到调度器 (scheduler) 和更精细的依赖管理。
第七部分:总结关键设计
实现 Vue 3 风格响应式依赖收集的核心步骤如下:
- 定义全局变量
activeEffect来追踪当前副作用。 - 设计三层存储结构
WeakMap(target) -> Map(key) -> Set(effects)来管理依赖关系。 - 使用
Proxy的get拦截器,在数据被读取时调用track函数,将activeEffect收集进对应key的依赖集合。 - 使用
Proxy的set拦截器,在数据被修改时调用trigger函数,从存储结构中取出所有依赖并逐一执行。 - 提供
effect函数作为副作用的入口,管理activeEffect的生命周期。
这套机制确保了视图(作为副作用)只会在其依赖的数据变化时精确更新,这是现代前端框架高性能的基石。通过 Proxy,我们无需在初始化时遍历对象的所有属性,也天然支持数组索引修改和 Map/Set 等集合类型,使得响应式系统更加完备和高效。

暂无评论,快来抢沙发吧!