先记住这个答案
ref 作为普通深层 reactive 对象的属性访问时,Vue 会自动读取它的内部值,并维持对应的响应式联系。给这种属性赋普通值,通常会更新原 ref 的 value;赋入另一个 ref 则会替换原来的引用关系。可是在 reactive 数组的索引元素或 Map 等集合中读取 ref 时,不进行同样的自动解包,需要显式 value。模板又有自己的解包位置规则,应与脚本中的代理行为分别理解。
- 普通深层对象属性可以自动解包 ref
- 数组索引、Map 值与浅层对象需要单独判断
- 赋普通值与赋新 ref 对原连接的影响不同
同一个 ref 放在不同位置,读取方式会不同
例如分页数 ref 放进页面 state 的普通属性中,读取 state.page 可以直接得到数字;把它放进 reactive 数组的第一个元素,读取 pages[0] 仍是 ref 容器。这个差别来自代理对不同位置的处理,不能根据容器都叫 reactive 就推断读取结果相同。
下面的例子把一个 ref 同时用于普通对象、数组、Map 和浅层对象,便于对照读取形式。真实项目不必刻意这样混用;接口越稳定,调用方越容易知道当前拿到的是 ref 还是普通值。通用工具处理未知输入时,也应明确是否接受 ref 并在合适边界规范化。
import { reactive, ref, shallowReactive } from 'vue';
const page = ref(2);
const state = reactive({ page });
const pages = reactive([page]);
const lookup = reactive(new Map([['current', page]]));
const shallow = shallowReactive({ page });
const values = [
state.page,
pages[0].value,
lookup.get('current')!.value,
shallow.page.value
];
state.page = 5;
const updatedPage = page.value;Vue 3 中 values 的四项都是 2,而对 state.page 赋普通值后 updatedPage 为 5。数组、Map 和浅层对象处保留显式 value;示例没有控制台输出。
赋普通值与替换 ref 是两种更新
原属性持有一个可写 ref 时,对自动解包后的对象属性写入普通值,可以更新原 ref 的内部值。若向该属性赋入另一个 ref,则会建立与新 ref 的联系,原 ref 不会因为名字相同继续跟随。依赖旧 ref 的其它调用方也不会自动改成持有新 ref。
TypeScript 中 reactive 的解包类型可能让某些直接替换 ref 的赋值不符合当前推导类型,不能因此编造运行时没有替换语义。应选择清楚的数据结构与类型边界,必要时通过明确 API 替换字段,而不是随意断言类型后让调用方猜测字段现在到底是什么。
模板中的便利规则不能推广到全部脚本
在模板中作为顶层绑定暴露的 ref,经常可以直接使用而不写 value;但嵌套在普通对象中的 ref、文本插值最终结果等位置又有具体规则。最可靠的判断方式是分别看模板表达式位置与脚本 API,而不是背一个“Vue 会自动去掉 value”的笼统结论。
shallowReactive 刻意保留浅层行为,不会像普通深层对象那样转换内部 ref。引入这类边界通常是为了控制外部状态集成或转换成本,混在深层对象里可能让访问规则难以预测。设计组合函数返回值时,应统一说明是返回 ref 集合,还是返回可直接访问属性的响应式对象。
容易答错的地方
- reactive 数组里的 ref 也能直接加一
- 数组索引元素不会按普通对象属性规则自动解包。应明确读取和写入元素的 value,避免对 ref 对象本身进行数值运算或错误替换。
- 给属性换新 ref 后,旧 ref 也会被永久关联
- 替换会改变这个属性使用的 ref,原来的容器不会自动变成新容器的别名。需要多个位置共享同一个值时,应明确共享哪个稳定 ref,而不是靠不断替换维持联系。
面试官还会怎么问?
readonly 与 shallowReadonly 的行为完全一样吗?
它们的深浅转换边界不同,不能仅因为都只读就推断内部 ref 解包相同。使用这类包装时,应按具体 API 的深层或浅层约定判断。
Map.get 返回数字还是 ref 怎样确定?
取决于你存进去的值和使用的 API。reactive Map 中存入 ref 时,读取该元素不会自动解包,因此需要 value;存入普通数字则直接得到数字。
组合函数返回多个 ref 可以直接解构吗?
如果返回的是普通对象中的多个 ref,解构得到的仍是那些 ref 容器,联系可以保留。若返回 reactive 对象的原始类型属性,普通解构得到的是当前值,应该分别处理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。