JavaScript 的元编程能力在 ES6 之后迎来了质的飞跃,其中 Proxy 和 Reflect 的引入,让我们能够以更优雅、更底层的方式干预对象的默认行为。Vue 3 正是借助这对组合,实现了比 Object.defineProperty 更强大、更自然的响应式系统。本文将带你从零开始,用 Proxy 和 Reflect 打造一个迷你响应式数据系统,深入理解元编程在前端框架中的实战应用。
为什么是 Proxy 和 Reflect?
在 Vue 2 时代,响应式依赖 Object.defineProperty,它有几个明显的痛点:
- 无法监听新增/删除属性:需要借助
Vue.set/Vue.delete这类特殊 API。 - 无法监听数组索引和 length 变化:Vue 2 通过重写数组方法(push、pop 等)来打补丁。
- 需要递归遍历所有属性:初始化时性能开销大。
Proxy 则从根本上解决了这些问题。它代理的是整个对象,而不是某个属性,因此可以拦截多达 13 种操作,包括 get、set、deleteProperty、has、ownKeys 等。
而 Reflect 是 Proxy 的最佳搭档。它提供了一套与 Proxy 拦截器一一对应的静态方法,用于执行对象默认行为。在 Proxy 的拦截器里,我们通常用 Reflect.get(target, key, receiver) 来代替直接访问 target[key],这样可以确保 this 指向正确,并保持默认行为的完整性。
核心思路:依赖收集与触发更新
响应式系统的核心只有两件事:
- 依赖收集(Track):当读取某个属性时,记录“谁”在用它。
- 触发更新(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?
原因有三:
- 语义一致性:
Reflect的方法与Proxy拦截器一一对应,代码更对称、易读。 - 正确的 receiver:
Reflect.get(target, key, receiver)会将receiver作为 getter 中的this,这在处理继承和访问器属性时至关重要。 - 返回值可靠:
Reflect.set返回布尔值表示是否成功,而直接赋值在严格模式下可能抛错,行为不一致。
在响应式系统中,receiver 的正确传递能保证:当代理对象作为原型被继承时,依赖收集仍然能正确关联到原始目标。
完整流程图
读取属性 → Proxy.get → track(收集 effect) → Reflect.get(返回默认值)
修改属性 → Proxy.set → Reflect.set(执行赋值) → trigger(通知 effect) → effect 重新执行
总结
通过 Proxy 和 Reflect,我们用不到 100 行代码实现了一个可用的响应式系统,它具备:
- 自动依赖收集与触发更新
- 支持新增/删除属性
- 支持数组
- 懒代理嵌套对象
这正是 Vue 3 响应式模块 @vue/reactivity 的核心原理。理解这套机制,不仅能让你更深入地使用 Vue 3,还能在需要时自己实现轻量级的状态管理库,甚至在 Node.js 中做数据校验、日志记录、权限控制等元编程任务。
Proxy 和 Reflect 是 JavaScript 元编程的基石,掌握它们,你就拥有了“改写语言默认行为”的能力。下一步,可以尝试为这个迷你系统加上 computed、watch 和 scheduler,让它更接近生产级实现。
未经允许不得转载:任鹏个人博客 » Proxy 与 Reflect 实战:用元编程打造响应式数据系统

