Vue 类实例 WeakMap 返回 undefined
类在WeakMap中保存原实例,代理方法的this可能不同。在集成入口采用markRaw或shallowRef,明确内部状态的UI更新方式。
第三方类在构造函数中用 WeakMap.set(this, privateData) 保存内部数据,原实例的方法正常;放进 Vue reactive() 后,方法中的 WeakMap.get(this) 却得到 undefined。先比较方法接收者:经代理访问的方法,this 可能已经是 Proxy,而 WeakMap 中保存的键仍是构造时的原实例。
下面按 Vue 3.5 的响应式规则说明。问题取决于类是否依赖实例身份,不能据此断言所有类或 WeakMap 都与 Vue 不兼容。
原实例和 Proxy 是两个不同的键
下面是用于说明身份关系的最小 JavaScript 示例:
import { reactive } from 'vue';
const privateData = new WeakMap();
class Counter {
constructor() {
privateData.set(this, { count: 0 });
}
read() {
return privateData.get(this)?.count;
}
}
const original = new Counter();
const wrapped = reactive(original);
// 预期:0、false、undefined
console.log(original.read(), original === wrapped, wrapped.read());
可选链让本例显示 undefined。真实库可能直接访问内部对象的属性,因而报读取 undefined 的错误;这是同一身份差异的后续表现,仍应核对实际调用栈。另一些调用方式可能丢失 this,不要把普通方法解绑定也误判成 Vue 代理问题。
对不支持代理的实例,在入口保留原身份
如果该类本来就不适合 Vue 的深层响应式包装,可在实例进入响应式状态前使用 markRaw():
import { markRaw, reactive } from 'vue';
const counter = markRaw(new Counter());
const state = reactive({ counter });
// 预期:true、0
console.log(state.counter === counter, state.counter.read());
这里沿用上文定义的 Counter。实际项目应在持有实例的集成入口处理,不要修改第三方库的 WeakMap 或假定所有调用方都能换用代理作为键。
markRaw 使这个对象不再被转换成 Proxy,也就不能指望其内部字段变化自动驱动 Vue 页面。库有事件或状态接口时,将需要展示的结果同步到应用自己的响应式数据,并按组件生命周期清理订阅。
另一种选择是用 shallowRef(instance) 保存实例,仅对 .value 的替换建立响应式关系。实例内部自行改变不会自动触发更新,仍要明确 UI 怎样得到新值;它不是深层 ref 的等价替换。
不要同时长期维护 raw 与 proxy 两套身份
toRaw() 能取回 Vue 代理对应的原对象,但它属于临时逃生接口。到处保留原对象、代理对象,再按不同路径调用同一个库,很容易让缓存、比较和订阅各自使用不同身份。
markRaw() 的标记也只作用于所标记对象。将它的一个未标记嵌套对象另外放进 reactive,该嵌套对象仍可能被代理;不能把根对象的标记写成递归豁免。
检查实例创建、放入状态、调用方法和销毁这四个入口,确认库操作始终使用它预期的接收者,页面通过明确的状态接口更新。保留原来的销毁与退订流程;方法重新能读到私有数据,并不证明旧实例或监听器已正确释放。


