文章目录

JavaScript Proxy实现Vue3响应式系统的依赖收集

发布于 2026-06-14 09:42:45 · 浏览 3 次 · 评论 0 条

JavaScript Proxy实现Vue3响应式系统的依赖收集

理解响应式系统的核心在于依赖收集。当数据变化时,我们需要知道“谁”在用这个数据,以便通知它们更新。Vue 3 利用 Proxy 实现了比 Vue 2 Object.defineProperty 更强大、更简洁的依赖收集机制。本文将从零开始,用代码一步步构建一个简化的核心依赖收集系统。

第一部分:为什么需要“依赖”以及它是什么

  1. 定义一个最简单的 App 函数,它内部使用了响应式数据。

    function App() {
      // 假设 `obj` 是一个响应式对象
      document.body.innerText = obj.message;
    }

    在这个函数执行时,它会读取 obj.message 的值。这里的 App 函数,就是 obj.message 这个数据的一个“依赖”。当 obj.message 改变时,我们需要重新执行 App

  2. 明确依赖收集的目标。我们需要在 App 函数执行并读取 obj.message 时,自动记录下:“App 依赖了 obj.message”。这样,当 obj.message 被修改时,我们就能找到所有记录过的依赖(比如 App)并触发它们。

第二部分:设计响应式系统的核心结构

我们需要三个核心部分来完成这个目标:

  • 响应式数据源 (reactive):将一个普通对象转换为响应式对象。
  • 当前正在执行的副作用函数 (activeEffect):一个全局变量,用来存储“正在收集依赖的函数”。
  • 依赖收集器 (track)依赖触发器 (trigger)
  1. 创建一个全局变量,用于存放“当前正在执行的副作用函数”。

    // 当前被激活的、正在收集依赖的副作用函数
    let activeEffect = null;
  2. 定义一个“副作用函数”注册器 effect。它的作用是执行一个函数,并在执行前将它设置为当前的 activeEffect

    function effect(fn) {
      // 将传入的 fn 标记为当前激活的副作用
      activeEffect = fn;
      // 立即执行一次 fn,在这个过程中,fn 内部对响应式数据的访问会被拦截并收集依赖
      fn();
      // 执行完毕后,清除激活状态(为简化,实际 Vue 会更复杂)
      activeEffect = null;
    }
  3. 设计依赖存储的数据结构。我们需要一个地方来存储 数据 -> 依赖 的映射。WeakMap 是理想选择,因为它以对象为键,且不会造成内存泄漏。

    // 使用 WeakMap 存储所有响应式对象的依赖关系
    // 结构: targetMap: { target -> depsMap: { key -> deps: Set } }
    const targetMap = new WeakMap();

第三部分:使用 Proxy 实现依赖收集与触发

这是最关键的一步。我们将用 Proxy 包装原始对象,拦截它的 getset 操作。

  1. 定义 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);
    }
  2. 定义 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());
    }
  3. 实现 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;
        }
      });
    }

第四部分:串联测试整个流程

现在,让我们把所有部分组装起来,并验证它是否工作。

  1. 创建一个响应式对象 state

    const state = reactive({ message: 'Hello Vue 3!', count: 0 });
  2. 定义两个副作用函数,它们会读取 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.messagestate.countProxyget 拦截器会被触发,调用 track 函数,将对应的副作用函数(A 或 B)收集到 state.messagestate.count 的依赖集合中。

  3. 修改 state 的属性,观察依赖是否被正确触发。

    // 修改 message,只应触发副作用 A
    state.message = 'Hello Proxy!';
    // 控制台输出:Message is: Hello Proxy!
    // 页面上 ID 为 `message` 的元素内容会更新
    
    // 修改 count,只应触发副作用 B
    state.count = 1;
    // 控制台输出:Count is: 1
    // 页面上 ID 为 `count` 的元素内容会更新

第五部分:理解“依赖收集”的精确含义

通过上述步骤,我们实现了精准的依赖收集。

  1. 回顾依赖收集发生在哪里。它发生在 Proxyget 拦截器中。只有当一个副作用函数(被 effect 包裹的函数)执行并读取某个响应式属性时,该属性才会把这个副作用记录为依赖。

  2. 对比直接赋值与依赖收集。如果我们在 effect 函数之外访问 state.message,比如 console.log(state.message),此时 activeEffectnulltrack 函数会直接返回,不会发生依赖收集。这保证了只有“真正用到数据”的代码才会被数据变化所通知。

  3. 理解依赖的存储位置。依赖被存储在 targetMap -> depsMap -> deps (Set) 这个三层结构中。WeakMap 的键是原始对象(target),所以当原始对象被垃圾回收时,其所有相关依赖也会被自动清除,避免了内存泄漏。

第六部分:处理嵌套对象与 computed

我们的简单实现已经具备了核心能力,但要达到 Vue 3 的健壮性,还需处理一些边界情况。

  1. 处理嵌套对象。如果 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 也具有了响应式能力。

  2. 实现计算属性 (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 风格响应式依赖收集的核心步骤如下:

  1. 定义全局变量 activeEffect 来追踪当前副作用。
  2. 设计三层存储结构 WeakMap(target) -> Map(key) -> Set(effects) 来管理依赖关系。
  3. 使用 Proxyget 拦截器,在数据被读取时调用 track 函数,将 activeEffect 收集进对应 key 的依赖集合。
  4. 使用 Proxyset 拦截器,在数据被修改时调用 trigger 函数,从存储结构中取出所有依赖并逐一执行。
  5. 提供 effect 函数作为副作用的入口,管理 activeEffect 的生命周期。

这套机制确保了视图(作为副作用)只会在其依赖的数据变化时精确更新,这是现代前端框架高性能的基石。通过 Proxy,我们无需在初始化时遍历对象的所有属性,也天然支持数组索引修改和 Map/Set 等集合类型,使得响应式系统更加完备和高效。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文