Vue3 响应式系统的实现原理
字数 1644 2025-11-03 00:19:05
Vue3 响应式系统的实现原理
描述:Vue3 的响应式系统是其核心特性之一,能够自动追踪数据变化并更新相关视图。与 Vue2 基于 Object.defineProperty 的实现不同,Vue3 使用了 ES6 的 Proxy 和 Reflect API 来构建响应式系统。理解其原理是掌握 Vue3 框架的关键。
解题过程:
-
核心目标与基本思路
- 目标:当数据(一个 JavaScript 对象)的属性被读取时,我们要记录下“谁”读取了它(这个过程称为依赖收集)。当数据的属性被修改时,我们要通知所有“读取过”它的地方进行更新(这个过程称为触发更新)。
- 思路:Vue3 使用
Proxy来“代理”一个普通对象。Proxy可以拦截(或称为“捕获”)对目标对象的各种基本操作,例如获取属性(get)、设置属性(set)、删除属性(deleteProperty)等。这样,我们就能够在这些操作发生时执行自定义的逻辑,从而实现依赖收集和触发更新。
-
关键角色:effect 与 reactive
reactive函数:这是 Vue3 中用于创建响应式对象的函数。它接收一个普通对象作为参数,并返回该对象的 Proxy 代理。effect函数:这是响应式系统的“副作用”函数。它接收一个函数(我们称之为fn)作为参数。effect会立即执行一次fn。关键在于,当fn在执行过程中读取了某个响应式对象的属性时,Vue 就能建立起fn(副作用)和这个属性(依赖)之间的联系。
-
逐步实现原理
-
步骤一:创建最简单的 reactive 函数
首先,我们创建一个能返回 Proxy 代理的函数。这个代理暂时只拦截get和set操作。function reactive(target) { return new Proxy(target, { get(obj, key) { // 拦截“读取”操作 console.log(`读取了属性 ${key}: ${obj[key]}`); return obj[key]; // 返回原始值 }, set(obj, key, value) { // 拦截“设置”操作 console.log(`修改了属性 ${key}: 从 ${obj[key]} 变为 ${value}`); obj[key] = value; // 设置原始值 return true; // 表示设置成功 } }); } // 测试 const state = reactive({ count: 0 }); state.count; // 控制台输出: "读取了属性 count: 0" state.count = 1; // 控制台输出: "修改了属性 count: 从 0 变为 1"现在我们已经可以拦截对对象的基本操作了。
-
步骤二:引入 effect 和依赖收集(核心)
现在我们需要一个机制来记录“哪个 effect 正在运行”,以及“哪些 effect 依赖于哪个响应式对象的哪个属性”。activeEffect全局变量:用于存储当前正在执行的effect函数。targetMap全局 WeakMap:用于建立依赖关系树。- 键(Key):响应式对象(
target)。 - 值(Value):一个 Map。
- 这个 Map 的键是响应式对象的属性名(
key)。 - 值是一个 Set,里面存储了所有依赖于这个
target.key的effect函数。
- 这个 Map 的键是响应式对象的属性名(
- 键(Key):响应式对象(
let activeEffect = null; // 当前活跃的 effect const targetMap = new WeakMap(); // 依赖存储仓库 function track(target, key) { // 依赖收集函数,在 get 拦截器内调用 if (!activeEffect) return; // 如果不在 effect 内读取,则不需要收集 // 1. 从 targetMap 中获取当前 target 对应的 depsMap let depsMap = targetMap.get(target); if (!depsMap) { depsMap = new Map(); targetMap.set(target, depsMap); } // 2. 从 depsMap 中获取当前 key 对应的 dep(一个 Set) let dep = depsMap.get(key); if (!dep) { dep = new Set(); depsMap.set(key, dep); } // 3. 将当前活跃的 effect 添加到 dep 中 dep.add(activeEffect); console.log(`跟踪依赖: ${key} -> 当前effect`); } function trigger(target, key) { // 触发更新函数,在 set 拦截器内调用 const depsMap = targetMap.get(target); if (!depsMap) return; const dep = depsMap.get(key); if (dep) { // 遍历 dep 集合,执行其中所有的 effect 函数 dep.forEach(effect => effect()); console.log(`触发更新: ${key}`); } } function effect(fn) { // 将传入的函数包装成一个 effect activeEffect = fn; // 设置当前活跃的 effect fn(); // 立即执行一次函数,执行过程中会触发 get,从而进行依赖收集 activeEffect = null; // 执行完毕后重置 } // 更新 reactive 函数 function reactive(target) { return new Proxy(target, { get(obj, key) { track(obj, key); // 读取时,收集依赖 return obj[key]; }, set(obj, key, value) { obj[key] = value; trigger(obj, key); // 设置时,触发更新 return true; } }); } -
步骤三:完整流程演示
让我们用一个完整的例子来串联整个流程。// 创建响应式对象 const user = reactive({ name: 'Alice', age: 25 }); // 定义一个 effect effect(() => { // 这个函数会立即执行 console.log(`用户名是: ${user.name}`); }); // 控制台立即输出: "用户名是: Alice" // 同时,在 effect 执行过程中,读取了 user.name, // 触发了 get 拦截器,track 函数被调用。 // 此时,targetMap 中建立了如下依赖关系: // targetMap: { user -> depsMap } // depsMap: { 'name' -> Set( [当前这个effect函数] ) } // 修改数据,触发更新 user.name = 'Bob'; // 1. 触发 set 拦截器 // 2. 调用 trigger(user, 'name') // 3. trigger 从 targetMap 中找到 user.name 对应的 dep(一个 Set) // 4. 遍历这个 Set,执行里面所有的 effect 函数 // 5. 控制台再次输出: "用户名是: Bob"
-
-
Vue3 的实际实现与优化
以上是最核心的原理。Vue3 源码中的实现(在@vue/reactivity包中)更加复杂和健壮,主要做了以下优化:- 嵌套的 effect:支持组件嵌套,通过一个
effect栈来管理。 - 调度器(Scheduler):允许控制
trigger触发后的执行时机,例如放入微任务队列中实现批量异步更新,这是 nextTick 原理的基础。 - 避免重复收集:确保同一个 effect 不会在同一个属性的 dep 中被重复收集。
- 分支切换的清理:例如在
v-if中,当条件变化时,需要清理掉不再需要的依赖。 - Ref & Computed:基于同样的响应式核心,
ref用.value属性解决了基本类型的响应式问题,computed是一个特殊的effect,具有懒计算和缓存值的特性。
- 嵌套的 effect:支持组件嵌套,通过一个
总结:Vue3 的响应式系统通过 Proxy 代理对象,在 get 操作时调用 track 进行依赖收集,在 set 操作时调用 trigger 触发更新。effect 函数是建立响应式数据与副作用函数之间桥梁的关键。整个系统围绕一个全局的 targetMap(WeakMap)来维护所有的依赖关系。