简短结论
原生的 new Proxy(target, handler)只能代理「它直接包裹的那一层对象」 ,对 target内部的嵌套对象,默认是"透传"的------返回的是裸对象,后续操作完全逃逸监听。
为什么会"听不到"嵌套对象?
const obj = { a: { b: 1 } };
const proxy = new Proxy(obj, {
get(t, k) { console.log('get', k); return Reflect.get(t, k); },
set(t, k, v) { console.log('set', k, v); return Reflect.set(t, k, v); }
});
proxy.a.b = 99;
// 只触发了一次 get(a),返回的是原始裸对象 { b: 1 }
// set(b) 永远不会触发!
执行 proxy.a.b = 99的过程:
-
先走
get(proxy, 'a')→ 拿到obj.a(原始{ b: 1 },不是 Proxy) -
然后对这个裸对象 执行
.b = 99→ 跟 Proxy 毫无关系
所以不是 Proxy "能力不够",而是它根本没有机会介入第二步------因为第一步返回的就不是代理对象。
✅ 解法:递归代理(Proxy Membrane 模式)
核心思路:在 get拦截器中,凡是读到的值是对象,就再给它套一层 Proxy,让整条访问链上的每一层都是代理过的:
function deepProxy(target, handler) {
// 缓存,避免重复代理 & 处理循环引用
const cache = new WeakMap();
function makeProxy(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (cache.has(obj)) return cache.get(obj);
const proxy = new Proxy(obj, {
get(t, key, receiver) {
const val = Reflect.get(t, key, receiver);
// 读到子对象 → 递归代理后返回
return (val !== null && typeof val === 'object')
? makeProxy(val)
: val;
},
set(t, key, value, receiver) {
const oldVal = t[key];
const result = Reflect.set(t, key, value, receiver);
handler?.onChange?.({
type: 'SET',
path: key,
oldValue: oldVal,
newValue: value
});
return result;
},
deleteProperty(t, key) {
const had = key in t;
const oldVal = t[key];
const result = Reflect.deleteProperty(t, key);
if (had) handler?.onChange?.({ type: 'DELETE', path: key, oldValue: oldVal });
return result;
}
});
cache.set(obj, proxy);
return proxy;
}
return makeProxy(target);
}
使用效果:
const state = deepProxy({ a: { b: 1 }, list: [10, 20] }, {
onChange: ({ type, path, oldValue, newValue }) =>
console.log(`[${type}] ${path}:`, oldValue, '→', newValue)
});
state.a.b = 99; // ✅ 能捕获!(经过递归代理的 a 的 set 触发)
state.list.push(30); // ⚠️ 数组的 push 本质是方法调用,set trap 不一定按你想的方式触发
state.a = { c: 2 }; // ✅ 外层 set 正常捕获(替换整个子对象引用)
两种"引用变化"要区分清楚
| 场景 | 能否被外层 Proxy 的 set捕获? |
说明 |
|---|---|---|
proxy.a = { c: 2 }(替换整个子对象引用) |
✅ 能 | 这是 proxy 自身的属性赋值,走 set(proxy, 'a', ...) |
proxy.a.b = 99(修改子对象内部属性) |
❌ 不能(除非递归代理) | 操作的是子对象,外层 proxy 根本碰不到 |
proxy.a = proxy.a(把子对象重新赋回) |
✅ 能触发 set | 虽然值没变但赋值行为本身被拦截 |
⚠️ 几个容易踩的坑
-
数组的
push、pop等方法 :它们内部会读写length,走的是方法调用路径而非简单set,做响应式系统时通常需要额外处理(Array的陷阱更复杂,Vue 3 用的也不是纯递归 Proxy 这么简单) -
必须用
receiver传进Reflect.get:如果对象上有 getter 或原型链继承,漏掉 receiver 会导致this指向错误:// ✅ 正确 const val = Reflect.get(t, key, receiver); // ❌ 危险 const val = t[key]; -
typeof null === 'object' → 判断时一定要加&& value !== null -
性能 :每次
get都判断+可能创建 Proxy,不加缓存的话同个引用被访问 N 次就产生 N 个 Proxy 实例。用WeakMap做缓存是标准做法
一句话总结
Proxy 本身是"单层"的------它只看守你交给它的那扇门。 想监听对象中的对象,就得在
get里把每个子对象也变成 Proxy (即 Proxy Membrane / 深代理),这也就是 Vue 3 的reactive()背后的核心思想。Proxy 不是不能,是需要你主动递归地"铺网"。