Proxy能够监听到对象中的对象的引用吗?

简短结论

原生的 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的过程

  1. 先走 get(proxy, 'a')→ 拿到 obj.a原始 { b: 1 },不是 Proxy

  2. 然后对这个裸对象 执行 .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 虽然值没变但赋值行为本身被拦截

⚠️ 几个容易踩的坑

  1. 数组的 pushpop等方法 :它们内部会读写 length,走的是方法调用路径而非简单 set,做响应式系统时通常需要额外处理(Array的陷阱更复杂,Vue 3 用的也不是纯递归 Proxy 这么简单)

  2. 必须用 receiver传进 Reflect.get :如果对象上有 getter 或原型链继承,漏掉 receiver 会导致 this指向错误:

    复制代码
    // ✅ 正确
    const val = Reflect.get(t, key, receiver);
    // ❌ 危险
    const val = t[key];
  3. typeof null === 'object' ​ → 判断时一定要加 && value !== null

  4. 性能 :每次 get都判断+可能创建 Proxy,不加缓存的话同个引用被访问 N 次就产生 N 个 Proxy 实例。用 WeakMap做缓存是标准做法


一句话总结

Proxy 本身是"单层"的------它只看守你交给它的那扇门。 ​ 想监听对象中的对象,就得在 get把每个子对象也变成 Proxy (即 Proxy Membrane / 深代理),这也就是 Vue 3 的 reactive()背后的核心思想。Proxy 不是不能,是需要你主动递归地"铺网"。

相关推荐
光影少年1 小时前
react 原理与进阶
前端·react.js·掘金·金石计划
kyrie281 小时前
Vue 全套性能优化方案
前端
charlie1145141911 小时前
现代C++指南:Lambda,让我们用另一种方式持有函数
开发语言·c++
Sour1 小时前
PDF翻译卡住不动怎么办?扫描件、OCR 和大文件排查清单
前端·pdf·ocr
ziyitty1 小时前
MiMoCode 配置 “Unrecognized key: mcpServers“ 问题解决方案
前端·chrome
大家的林语冰1 小时前
连 Markdown 都不放过,Rust 在前端基建杀疯了,万物皆可“锈化“!
前端·javascript·markdown
参宿72 小时前
CSS 悬挂空白与选区溢出
前端·css
想吃火锅10052 小时前
【前端手撕】instanceof
前端·javascript·原型模式
один but you2 小时前
const和constexpr常量表达式
java·前端·javascript