先记住这个答案
reactive 需要对象作为代理目标,不能直接把一个数字变量的赋值变成可追踪操作。ref 提供带 value 属性的容器,让读取与赋值经过 Vue 能追踪的入口,因此能表示数字、字符串等原始值,也能保存对象。它的意义不只是补上原始类型,还便于保留稳定的容器身份并整体替换内部值。选择时应看数据如何更新和传递,而不是简单规定对象只能用 reactive。
- 原始变量赋值没有对象属性入口供代理追踪
- ref 容器身份可保持稳定,value 可以整体替换
- ref 也能保存对象,普通 ref 会进行深层响应式转换
为什么包一层对象有实际作用
假设分页状态是一个数字 page,普通赋值只改变当前变量绑定,并没有一个可被 Vue 拦截的属性写入。使用 ref 后,修改 page.value 会经过响应式入口;依赖这个值的计算就能知道输入发生变化。容器并非为了多写一个属性,而是为原始值建立可追踪的读写位置。
使用 reactive({ page: 1 }) 也能表达分页状态,因为这里被代理的是对象,page 是它的属性。两种方式都可以成立,区别在于你希望把分页作为独立值传递,还是作为一组状态中的字段维护。没有必要为证明某种 API 更先进而强行改写清楚的数据模型。
对象整体替换时 ref 提供稳定入口
搜索结果可能在每次请求后整体替换为一个新对象。若其它逻辑持有同一个 result ref,它们下一次读取 result.value 时就能得到新对象。普通 ref 中的对象会被转换成深层响应式代理,因此内部字段变化也可能被追踪;需要保留外部对象原样身份时再考虑 shallowRef 等明确边界。
对 let state = reactive(...) 重新赋一个新代理,只是替换了那个局部变量;已经拿到旧代理的组合函数并不会自动改成持有新代理。可以维持原代理并按业务规则更新字段,也可以从一开始通过 ref 提供可替换对象的稳定入口,选择应与数据生命周期一致。
传递 ref 与传递当前值的效果不同
把 page.value 作为参数传给普通函数,传出去的是这次读取到的数字,不会自动附带后续变化关系。需要接收方持续读取时,可以传 ref、getter 或包含该字段的响应式对象,并让接收方在响应式计算中真正读取它。接口契约应说明接收静态值还是动态来源。
模板会对某些位置的 ref 自动解包,所以展示时看起来可以直接写 page,但脚本中的对象、数组和集合有各自规则。不要因为一个模板表达式省略了 value,就在 JavaScript 计算中把 ref 对象当成数字相加。排查类型与更新问题时,先确认拿到的是容器还是内部值。
容易答错的地方
- ref 只能保存字符串、数字和布尔值
- ref 可以保存对象,并且普通 ref 会对对象进行深层响应式转换。需要浅层持有外部实例时,应明确选择对应 API,而不是用“对象不能放 ref”解释行为。
- 把 ref.value 传出去就会一直保持最新
- 传出原始值时只是传递当前快照。持续响应需要接收方保留可读取的来源,并在响应式上下文内访问,不能期待值本身带有隐式订阅。
面试官还会怎么问?
ref 和 reactive 可以混合使用吗?
可以,但要清楚属性自动解包与容器身份的边界。混合不是问题,接口让调用方猜测当前需要读 value 还是直接读字段,才容易造成维护成本。
为什么 const 声明的 ref 仍能更新?
const 固定的是变量绑定到哪个 ref 对象,并不冻结这个对象的 value。修改 value 与把变量重新绑定到另一个 ref,是不同的操作。
什么时候考虑 shallowRef?
需要只在整体替换时触发更新,或希望保留第三方实例、不可变数据等内部结构时可以评估。浅层持有不会自动追踪任意深层修改,调用方必须理解这个约定。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。