Vue3 响应式系统的实现原理
字数 1644 2025-11-03 00:19:05

Vue3 响应式系统的实现原理

描述:Vue3 的响应式系统是其核心特性之一,能够自动追踪数据变化并更新相关视图。与 Vue2 基于 Object.defineProperty 的实现不同,Vue3 使用了 ES6 的 Proxy 和 Reflect API 来构建响应式系统。理解其原理是掌握 Vue3 框架的关键。

解题过程

  1. 核心目标与基本思路

    • 目标:当数据(一个 JavaScript 对象)的属性被读取时,我们要记录下“谁”读取了它(这个过程称为依赖收集)。当数据的属性被修改时,我们要通知所有“读取过”它的地方进行更新(这个过程称为触发更新)。
    • 思路:Vue3 使用 Proxy 来“代理”一个普通对象。Proxy 可以拦截(或称为“捕获”)对目标对象的各种基本操作,例如获取属性(get)、设置属性(set)、删除属性(deleteProperty)等。这样,我们就能够在这些操作发生时执行自定义的逻辑,从而实现依赖收集和触发更新。
  2. 关键角色:effect 与 reactive

    • reactive 函数:这是 Vue3 中用于创建响应式对象的函数。它接收一个普通对象作为参数,并返回该对象的 Proxy 代理。
    • effect 函数:这是响应式系统的“副作用”函数。它接收一个函数(我们称之为 fn)作为参数。effect 会立即执行一次 fn。关键在于,当 fn 在执行过程中读取了某个响应式对象的属性时,Vue 就能建立起 fn(副作用)和这个属性(依赖)之间的联系。
  3. 逐步实现原理

    • 步骤一:创建最简单的 reactive 函数
      首先,我们创建一个能返回 Proxy 代理的函数。这个代理暂时只拦截 getset 操作。

      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.keyeffect 函数。
      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"
      
  4. Vue3 的实际实现与优化
    以上是最核心的原理。Vue3 源码中的实现(在 @vue/reactivity 包中)更加复杂和健壮,主要做了以下优化:

    • 嵌套的 effect:支持组件嵌套,通过一个 effect 栈来管理。
    • 调度器(Scheduler):允许控制 trigger 触发后的执行时机,例如放入微任务队列中实现批量异步更新,这是 nextTick 原理的基础。
    • 避免重复收集:确保同一个 effect 不会在同一个属性的 dep 中被重复收集。
    • 分支切换的清理:例如在 v-if 中,当条件变化时,需要清理掉不再需要的依赖。
    • Ref & Computed:基于同样的响应式核心,ref.value 属性解决了基本类型的响应式问题,computed 是一个特殊的 effect,具有懒计算和缓存值的特性。

总结:Vue3 的响应式系统通过 Proxy 代理对象,在 get 操作时调用 track 进行依赖收集,在 set 操作时调用 trigger 触发更新effect 函数是建立响应式数据与副作用函数之间桥梁的关键。整个系统围绕一个全局的 targetMap(WeakMap)来维护所有的依赖关系。

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 操作。 现在我们已经可以拦截对对象的基本操作了。 步骤二:引入 effect 和依赖收集(核心) 现在我们需要一个机制来记录“哪个 effect 正在运行”,以及“哪些 effect 依赖于哪个响应式对象的哪个属性”。 activeEffect 全局变量 :用于存储当前正在执行的 effect 函数。 targetMap 全局 WeakMap :用于建立依赖关系树。 键(Key):响应式对象( target )。 值(Value):一个 Map。 这个 Map 的键是响应式对象的属性名( key )。 值是一个 Set,里面存储了所有依赖于这个 target.key 的 effect 函数。 步骤三:完整流程演示 让我们用一个完整的例子来串联整个流程。 Vue3 的实际实现与优化 以上是最核心的原理。Vue3 源码中的实现(在 @vue/reactivity 包中)更加复杂和健壮,主要做了以下优化: 嵌套的 effect :支持组件嵌套,通过一个 effect 栈来管理。 调度器(Scheduler) :允许控制 trigger 触发后的执行时机,例如放入微任务队列中实现批量异步更新,这是 nextTick 原理的基础。 避免重复收集 :确保同一个 effect 不会在同一个属性的 dep 中被重复收集。 分支切换的清理 :例如在 v-if 中,当条件变化时,需要清理掉不再需要的依赖。 Ref & Computed :基于同样的响应式核心, ref 用 .value 属性解决了基本类型的响应式问题, computed 是一个特殊的 effect ,具有懒计算和缓存值的特性。 总结 :Vue3 的响应式系统通过 Proxy 代理对象,在 get 操作时调用 track 进行 依赖收集 ,在 set 操作时调用 trigger 触发更新 。 effect 函数是建立响应式数据与副作用函数之间桥梁的关键。整个系统围绕一个全局的 targetMap (WeakMap)来维护所有的依赖关系。