proxy 默认仅浅层拦截,嵌套属性变更不触发set;需在get中递归代理并用weakmap缓存防重复与循环引用,notify须异步节流避免死链。

Proxy 默认只做浅层拦截,无法自动响应嵌套对象属性变化;循环引用会导致无限递归代理、内存泄漏或栈溢出。
为什么 set 拦截器对深层赋值无效
当你写 person.address.city = 'Beijing' 时,set 只在 person 这一层触发,address 和 city 的变更完全绕过 Proxy —— 因为 address 是普通对象,没被代理过。
常见错误现象:observe(print) 注册后,改 person.name 能触发,但改 person.profile.avatar.url 完全静默。
- 必须对每个嵌套对象也调用
observable,否则不进拦截逻辑 -
Reflect.set返回true不代表目标被代理,只说明赋值成功 - 原始值(
string/number/boolean)无法被代理,只能靠外层对象的set通知
如何递归代理嵌套对象并避免重复代理
核心是:每次访问属性(get)时判断返回值是否为对象,若是且未被代理过,就用 observable 包一层再返回。
安全更新和维护 CLI Proxy API(CPA)部署与配置。用于 CPA 镜像升级、配置变更、认证目录兼容修复、上线验证与回滚。适用于用户提到“CPA 更新/升级/配置改了/容器重建/回滚”等场景。
关键点在于用 WeakMap 缓存已代理对象,既避免循环引用导致的重复代理,又防止内存泄漏(WeakMap 键是弱引用)。
const proxyCache = new WeakMap();
const observable = obj => {
if (obj === null || typeof obj !== 'object') return obj;
if (proxyCache.has(obj)) return proxyCache.get(obj);
const handler = {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 仅对 plain object / array 递归代理,跳过 Date、RegExp、函数等
if (res != null && typeof res === 'object' && !res[Symbol.toStringTag]) {
const proxied = observable(res);
Reflect.set(target, key, proxied, receiver);
return proxied;
}
return res;
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
notify(); // 统一通知所有观察者
return result;
}
};
const proxy = new Proxy(obj, handler);
proxyCache.set(obj, proxy);
return proxy;
};
循环引用场景下 notify 如何不爆栈
如果 a.b = b 且 b.a = a,递归代理时会死循环。上面的 WeakMap 缓存能防住首次代理,但 notify() 若直接遍历整个嵌套结构触发回调,仍可能因观察者内部又修改了被观察对象而陷入“通知→修改→再通知”死链。
-
notify()必须异步节流,推荐用Promise.resolve().then(() => {...})做微任务批次合并 - 观察者函数内禁止同步修改被观察对象,否则触发重入 —— 这不是 Proxy 的问题,是使用约束
- 深层变更时,
notify()不应尝试 diff 路径,只做全局广播;精细路径通知需额外实现依赖收集(如 Vue 3 的track/trigger)
真正需要深度响应时,别硬刚 Proxy
Proxy 实现深度观察者模式可行,但复杂度陡增:要处理 Array.prototype.push、Map/Set、Proxy.revocable、in 操作符、deleteProperty 等边界情况。实际项目中,更稳妥的做法是:
- 用
JSON.parse(JSON.stringify(obj))做不可变更新(适合配置类数据) - 引入
immer,它底层用 Proxy + 代理快照,对外暴露简洁 API - 若必须手写,优先限制观察深度(如只到第二层),或约定只观察扁平化状态树
最易被忽略的一点:WeakMap 缓存只对同一对象实例有效;如果你每次 observable({x: {y: 1}}) 都传新对象,缓存无意义 —— 这时候就得配合状态管理器做引用复用。










