Proxy 与 Reflect 实战:用元编程打造响应式数据系统

JavaScript 的元编程能力在 ES6 之后迎来了质的飞跃,其中 ProxyReflect 的引入,让我们能够以更优雅、更底层的方式干预对象的默认行为。Vue 3 正是借助这对组合,实现了比 Object.defineProperty 更强大、更自然的响应式系统。本文将带你从零开始,用 ProxyReflect 打造一个迷你响应式数据系统,深入理解元编程在前端框架中的实战应用。

为什么是 Proxy 和 Reflect?

在 Vue 2 时代,响应式依赖 Object.defineProperty,它有几个明显的痛点:

  • 无法监听新增/删除属性:需要借助 Vue.set / Vue.delete 这类特殊 API。
  • 无法监听数组索引和 length 变化:Vue 2 通过重写数组方法(push、pop 等)来打补丁。
  • 需要递归遍历所有属性:初始化时性能开销大。

Proxy 则从根本上解决了这些问题。它代理的是整个对象,而不是某个属性,因此可以拦截多达 13 种操作,包括 getsetdeletePropertyhasownKeys 等。

ReflectProxy 的最佳搭档。它提供了一套与 Proxy 拦截器一一对应的静态方法,用于执行对象默认行为。在 Proxy 的拦截器里,我们通常用 Reflect.get(target, key, receiver) 来代替直接访问 target[key],这样可以确保 this 指向正确,并保持默认行为的完整性。

核心思路:依赖收集与触发更新

响应式系统的核心只有两件事:

  1. 依赖收集(Track):当读取某个属性时,记录“谁”在用它。
  2. 触发更新(Trigger):当修改某个属性时,通知所有依赖它的“谁”重新执行。

这里的“谁”通常是一个副作用函数(effect),比如组件的渲染函数、计算属性或 watch 回调。

我们用 Proxy 拦截 get 来收集依赖,拦截 set 来触发更新。用 Reflect 来执行默认的读写行为。

动手实现:迷你响应式系统

1. 定义全局状态

// 当前正在执行的 effect
let activeEffect = null;

// 依赖存储结构:target -> key -> Set<effect>
const targetMap = new WeakMap();

// 用于暂存 effect 的栈,处理嵌套 effect
const effectStack = [];

2. 实现 effect

function effect(fn) {
  const effectFn = () => {
    try {
      activeEffect = effectFn;
      effectStack.push(effectFn);
      // 执行函数,触发 get 拦截,完成依赖收集
      return fn();
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1] || null;
    }
  };
  effectFn();
  return effectFn;
}

3. 依赖收集 track

function track(target, key) {
  if (!activeEffect) return;

  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }

  deps.add(activeEffect);
}

4. 触发更新 trigger

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const deps = depsMap.get(key);
  if (!deps) return;

  // 复制一份,避免 effect 执行中修改集合导致死循环
  const effectsToRun = new Set(deps);
  effectsToRun.forEach(effectFn => effectFn());
}

5. 创建 reactive

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key);
      // 用 Reflect 执行默认读取行为
      const result = Reflect.get(target, key, receiver);
      // 如果结果还是对象,递归代理(懒代理,性能更优)
      if (typeof result === 'object' && result !== null) {
        return reactive(result);
      }
      return result;
    },

    set(target, key, value, receiver) {
      const oldValue = target[key];
      // 用 Reflect 执行默认赋值行为
      const result = Reflect.set(target, key, value, receiver);
      // 值变化时才触发更新
      if (oldValue !== value) {
        trigger(target, key);
      }
      return result;
    },

    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey && result) {
        trigger(target, key);
      }
      return result;
    }
  });
}

6. 测试一下

const state = reactive({ count: 0, name: 'Vue' });

effect(() => {
  console.log(`count is: ${state.count}`);
});
// 输出: count is: 0

state.count++;
// 输出: count is: 1

state.name = 'React';
// 没有输出,因为 effect 没有依赖 name

进阶:处理数组与嵌套结构

Proxy 天然支持数组,因为数组的索引访问和 length 修改都会走 get / set 拦截器。但要注意一个细节:当数组调用 push 时,会先读取 length,再设置索引,再设置 length。这会导致 length 被收集为依赖,可能引发不必要的更新。

Vue 3 的做法是在 trigger 时对数组的 length 做特殊处理,或者通过判断 key 类型来优化。对于我们的迷你实现,可以简单地在 track 时忽略数组的 length 属性:

get(target, key, receiver) {
  if (key !== 'length' || !Array.isArray(target)) {
    track(target, key);
  }
  // ...
}

嵌套对象方面,我们已经在 get 中做了懒代理:只有访问到嵌套对象时才会递归创建 Proxy,这比 Vue 2 的初始化递归遍历要高效得多。

Reflect 的价值在哪里?

你可能会问:既然 target[key] 也能读写,为什么非要用 Reflect

原因有三:

  1. 语义一致性Reflect 的方法与 Proxy 拦截器一一对应,代码更对称、易读。
  2. 正确的 receiverReflect.get(target, key, receiver) 会将 receiver 作为 getter 中的 this,这在处理继承和访问器属性时至关重要。
  3. 返回值可靠Reflect.set 返回布尔值表示是否成功,而直接赋值在严格模式下可能抛错,行为不一致。

在响应式系统中,receiver 的正确传递能保证:当代理对象作为原型被继承时,依赖收集仍然能正确关联到原始目标。

完整流程图

读取属性 → Proxy.get → track(收集 effect) → Reflect.get(返回默认值)
修改属性 → Proxy.set → Reflect.set(执行赋值) → trigger(通知 effect) → effect 重新执行

总结

通过 ProxyReflect,我们用不到 100 行代码实现了一个可用的响应式系统,它具备:

  • 自动依赖收集与触发更新
  • 支持新增/删除属性
  • 支持数组
  • 懒代理嵌套对象

这正是 Vue 3 响应式模块 @vue/reactivity 的核心原理。理解这套机制,不仅能让你更深入地使用 Vue 3,还能在需要时自己实现轻量级的状态管理库,甚至在 Node.js 中做数据校验、日志记录、权限控制等元编程任务。

ProxyReflect 是 JavaScript 元编程的基石,掌握它们,你就拥有了“改写语言默认行为”的能力。下一步,可以尝试为这个迷你系统加上 computedwatchscheduler,让它更接近生产级实现。

未经允许不得转载:任鹏个人博客 » Proxy 与 Reflect 实战:用元编程打造响应式数据系统

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏