DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

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,该嵌套对象仍可能被代理;不能把根对象的标记写成递归豁免。

检查实例创建、放入状态、调用方法和销毁这四个入口,确认库操作始终使用它预期的接收者,页面通过明确的状态接口更新。保留原来的销毁与退订流程;方法重新能读到私有数据,并不证明旧实例或监听器已正确释放。