简短结论
原生的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 的 | 说明 |
|---|---|---|
| ✅能 | 这是 proxy 自身的属性赋值,走 |
| ❌不能(除非递归代理) | 操作的是子对象,外层 proxy 根本碰不到 |
| ✅ 能触发 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 不是不能,是需要你主动递归地"铺网"。