Vue Map 中嵌套 ref,访问 foo.value 却是 undefined
Map直接保存ref与返回对象的ref属性解包不同。对照运行时形态,区分深层、浅层与raw值,修正具体读取及更新入口。
reactive(new Map([['count', { foo: ref(0) }]])) 中,读取 map.get('count').foo.value 得到 undefined,去掉 .value 反而得到 0。先区分 Map 的直接值和这个值对象的属性:Map 直接保存的 ref 不会自动解包,而深层响应式对象上的 ref 属性会。
以下按 Vue 3.5 的深层 reactive 行为说明。类型提示与某个旧版本运行结果不一致,需要单独核对工具和类型声明,不能只凭提示断定运行时一定仍是 Ref。
两种 Map 值需要不同的访问方式
import { reactive, ref, isRef } from 'vue';
const direct = reactive(new Map([
['count', ref(0)],
]));
const nested = reactive(new Map([
['count', { foo: ref(0) }],
]));
// 预期:true、0
console.log(isRef(direct.get('count')), direct.get('count').value);
const entry = nested.get('count');
// 预期:false、0
console.log(isRef(entry.foo), entry.foo);
第一个 Map 的直接值就是 ref,保留 .value。第二个 Map 返回一个深层响应式对象,再访问它的 foo 属性时,ref 已被解包成数字;数字的 .value 自然不是需要的值。
本例保证 count 键存在。真实数据可能没有这个键,Map.get() 会返回 undefined,首先要按业务处理缺失项,不能把缺失项异常也归为 ref 解包。
在入口确定值的形态,避免混着访问
如果业务约定每个 Map 值就是一个 ref,就统一按直接 ref 的形式读取。如果 Map 值是记录对象,由深层响应式对象管理字段,就按照解包后的属性读取,不给每个字段机械追加 .value。
shallowReactive(new Map(...)) 不会替返回的普通对象继续建立深层代理;标记为 raw 的对象也可能保留其内部 ref。采用这些接口时,需要重新对照真实结构,不能把上面的 nested 结果套到所有 Map。
必要时在局部调试入口检查 isRef(value)、键是否存在及实际 Vue 版本,减少对类型断言的依赖。不要在生产循环中输出用户数据,更不要用 as any 隐藏整个数据模型的差异。
修改字段也要沿着同一模型
深层响应式对象上已链接 ref 的普通属性,写入新数值可以更新对应 ref;直接保存于 Map 的 ref 则仍通过 .value 操作。将一个新的 ref 赋给对象属性,会替换原来的 ref 关联,旧 ref 不会继续代表这个属性。
先保留原有值类型,再修正具体读取点。用直接 ref、对象中的 ref、缺失键和浅层 Map 分别验证读取及更新,确认模板和业务逻辑采用同一约定。删除所有 .value 或给所有字段补 .value,都可能修好这一行、同时破坏其他形态的数据。


