ref 和 reactive 有什么区别?该怎么选择?
简化版
ref 可以包装任意值,访问和修改时用 .value;reactive 只能代理对象,返回对象代理。基本类型用 ref,对象状态可以用 reactive;需要解构保持响应式时要配合 toRefs。
详细版
const count = ref(0);
count.value++;
const user = reactive({ name: 'Tom' });
user.name = 'Jerry';
区别:
ref通过.value持有值。reactive返回 Proxy,直接访问属性。reactive解构会丢失响应式。
const state = reactive({ count: 0 });
const { count } = state; // count 不是响应式引用
需要:
const { count } = toRefs(state);
实践中,组合式函数返回多个状态时常用 ref 或 toRefs,避免调用方解构丢响应式。
完整版教学
一、ref 为什么需要 value
基本类型不是对象,Proxy 不能直接代理数字或字符串的读写。Vue 用一个对象包装它:
{ value: 0 }
读取和修改 .value 时就能收集和触发依赖。
二、reactive 更像对象状态容器
reactive 适合一组相关状态:
const form = reactive({
username: '',
password: '',
});
模板和方法里直接 form.username,可读性很好。
reactive 返回的是原对象的 Proxy,同一个代理上的属性访问才能进入追踪链路。若把变量整体重新赋成另一个 reactive 对象,先前持有旧代理的计算、函数或组件不会自动改指向,因此需要整体替换的状态通常用 ref 更稳。
三、解构丢失响应式的原因
响应式依赖 Proxy 的 get/set。解构后拿到的是普通值,不再经过原代理对象的访问器。
const { username } = form;
如果 username 是字符串,这就是普通字符串。toRef/toRefs 会把属性转成 ref,保留和原对象属性的连接。
四、选择建议
单值、异步结果、开关状态用 ref 很自然。复杂表单或一组强相关字段用 reactive。组合式函数如果要返回状态给外部解构,返回 ref 更稳。
五、面试追问与工程落地
常见追问是“ref 包对象和 reactive 对象有什么区别”。ref({}) 内部对象默认也会被转成响应式,访问时多一层 .value;reactive({}) 直接返回代理对象。对于需要整体替换的对象,ref 更方便;对于稳定对象结构,reactive 写起来更自然。
还会问模板里为什么不用 .value。Vue 模板会自动解包 ref,setup 返回给模板的 ref 可以直接使用。但在普通 JS/TS 逻辑中必须写 .value。这个差异是新手常见 bug 来源。
工程里组合式函数返回值建议谨慎。如果返回 reactive 对象,调用方解构可能丢响应式;如果返回一组 ref,解构更安全。很多 composable 会返回 { count, loading, refresh },其中状态都是 ref。
六、精确区分替换、解构和自动解包边界
reactive 返回的 Proxy 身份承载依赖关系,直接把变量重新赋成另一个 reactive 对象,会让原先持有旧代理的消费者继续留在旧连接上。ref 把连接固定在 .value 这个属性上,因此整体替换对象只会触发同一个 ref,适合请求结果、当前选中项等可整体替换状态。官方文档也因 reactive 的这些限制,建议把 ref 作为声明状态的主要 API。
const state = reactive({ count: 0, nested: { value: 1 } })
let { count, nested } = state
count++ // 普通数字,与 state.count 失联
nested.value++ // nested 本身仍是响应式代理
const result = ref({ id: 1 })
result.value = { id: 2 } // 整体替换仍通过同一个 ref 触发
| 操作 | ref | reactive |
|---|---|---|
| 原始类型 | 支持 | 不支持 |
| 对象深层响应式 | 默认支持 | 默认支持 |
| 整体替换 | 给 .value 赋值即可 | 替换变量会丢原连接 |
| 解构原始属性 | ref 自身可安全解构 | 普通值与代理失联 |
| 模板使用 | 顶层场景会自动解包 | 直接访问代理属性 |
假设 composable 返回 3 个状态字段,直接返回 reactive 对象后调用方一解构就可能得到 3 个失联的原始值;先 toRefs(state) 会创建 3 个指向原属性的 ref。代价是 API 中出现更多 .value,收益是连接关系明确且允许安全解构。
ref 的模板自动解包也有边界:普通 JavaScript 中必须使用 .value,集合元素或某些深层表达式也不应假设会无条件解包。团队代码应以实际类型和模板编译规则为准,而不是记成“模板里永远不用 value”。
选择锚点:会不会整体替换、会不会被解构、是不是原始值;这三个问题通常比“对象一律 reactive”更可靠。
七、常见误区与追问
- 误区:reactive 可以直接包装 number 和 string。 它只适用于对象、数组和 Map/Set 等对象类型,原始值应使用 ref。
- 误区:从 reactive 解构出的任何值都会彻底失去响应式。 原始属性会失联;若取出的是已代理嵌套对象,其内部属性仍可能响应式,但与原属性整体替换不再同步。
- 误区:ref 包对象只会监听 value 的整体替换。 普通 ref 会把对象值深度转为响应式,需要浅层语义时用 shallowRef。
- 追问:为什么 ref 需要 value? 原始值无法被 Proxy 直接拦截,包装对象的 value getter/setter 提供稳定的追踪位置。
- 追问:为什么 reactive 不适合整体替换? 消费者跟踪的是原 Proxy 上的属性访问,变量指向新 Proxy 不会自动改掉其他引用。
- 追问:toRefs 做了什么? 它为 reactive 对象的各属性创建保持双向连接的 ref,方便解构或返回给外部。
- 追问:ref 和 shallowRef 如何选择? 需要追踪嵌套修改用 ref;大型不可变数据或第三方实例只需根替换时用 shallowRef。
八、加强记忆
ref 是“盒子”,值在 .value;reactive 是“代理对象”,属性直接读写。解构 reactive 要小心,想安全解构就用 toRefs。
再用原始值、整体替换和解构边界三个问题做选择,比机械地按“简单或复杂数据”分类更准确。