ref、reactive、toRefs 到底该用哪个?我把三个反例都写了一遍
摘要:ref 和 reactive 不是"哪个更好",是设计前提不同。三个反例,把 toRefs 为什么存在讲透。
上一篇《我重写了 Vue3 的响应式》里,我讲了 ref 为什么必须 .value——那是从"语言层面"解释的。这篇换个角度:实际项目里,ref、reactive、toRefs 到底该怎么选。
先给结论
这三个 API 不是"三选一"的关系,它们解决的是三个不同层面的问题:
| API | 解决的问题 |
|---|---|
reactive | 把一个对象变成响应式 |
ref | 把任意值(包括基本类型)变成响应式 |
toRefs | 让 reactive 对象的属性解构之后仍然是响应式 |
所以"ref 还是 reactive"这个问题本身问错了——它们不是竞品,是互补的。
真正需要记住的是这句话:对象用 reactive 少写 .value;团队若统一风格,全用 ref 也完全合理。选型看场景,不是谁更高级。reactive 有两个绕不开的限制,所以 ref 往往更通用;而 toRefs 解决的是一个完全独立的问题:解构。
reactive 的两个致命限制
限制 1:reactive 不能接基本类型
const count = reactive(0) // ❌ 控制台警告:value cannot be made reactive
原因上一篇讲过了:Proxy 的第一个参数必须是对象。基本类型(数字、字符串、布尔)没法被代理。
所以基本类型只能用 ref:
const count = ref(0) // ✅ 包了一层 { value: 0 }
限制 2:reactive 解构后会丢响应式
这是 90% 的人踩过的坑,也是 toRefs 最常见的存在理由(后面还会看到:拆给组合式函数、读 props 也是):
const state = reactive({ count: 0, name: 'Vue' })
// 解构(用 let 才能 ++;用 const 解构后无法重新赋值)
let { count, name } = state
count++ // ❌ 视图不会更新
为什么?因为解构出来的 count 是一个普通的数字 0,它和原来的 state.count 已经没有关系了。state.count 是被 Proxy 拦截的属性,但 count 只是一个值拷贝。
flowchart LR
subgraph 解构
S1[reactive state] -->|解构| C1["count: 0<br/>❌ 普通值,断开连接"]
end
subgraph toRefs
S2[reactive state] -->|toRefs| C2["count: ref<br/>✅ 指向原对象"]
end
toRefs 做的事不是"把 reactive 转成 ref",而是把 reactive 的每个属性转成一个指向原对象的 ref——你改这个 ref,改的还是原对象。
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)
count.value++ // ✅ 视图会更新
三个反例
反例 1:以为 reactive 解构一定会丢响应式
const state = reactive({ userInfo: { name: '' } })
const { userInfo } = state
userInfo.name = 'xxx' // ✅ 会更新
这个会更新。因为 userInfo 是一个对象的引用,解构出来的是同一个对象的引用。
但:
const state = reactive({ count: 0 })
let { count } = state
count++ // ❌ 不会更新
解构基本类型会丢,解构对象引用不会。 这个差异很多人没意识到。
反例 2:ref 用在对象上,多写了一层 .value
// ❌ 不是不能用,只是脚本里多一层 .value
const user = ref({ name: 'Vue', age: 3 })
user.value.name = 'React' // 只有一层 .value,再访问属性
对象直接用 reactive 更省事:
// ✅
const user = reactive({ name: 'Vue', age: 3 })
user.name = 'React'
补充:模板里 ref 会自动解包,写 user.name 就行,不用写 user.value.name。多出来的 .value 主要烦在 script 里。
反例 3:return 时把 reactive 拆散了,模板丢响应式
容易踩的不是「模板要不要写 .value」(模板里 ref 会自动解包),而是 return 时把响应式拆坏了:
// ❌ 展开 / 解构后再 return,拿到的是普通值
const state = reactive({ count: 0 })
return { ...state }
// 或:const { count } = state; return { count }
<!-- 点按钮改 count,视图不会更新 -->
<button @click="count++">{{ count }}</button>
正确写法:把整个代理 return 出去,或用 toRefs 保住连接:
// ✅ 方案 A:整体 return
const state = reactive({ count: 0 })
return { state }
// 模板:state.count
// ✅ 方案 B:要解构就用 toRefs
const state = reactive({ count: 0 })
return { ...toRefs(state) }
// 模板:count(自动解包,不用 .value)
到底该用哪个?
| 场景 | 推荐 | 理由 |
|---|---|---|
| 基本类型(数字、字符串、布尔) | ref | reactive 接不了 |
| 纯对象、表单数据 | reactive 或 ref | 前者少写 .value;团队统一用 ref 也行 |
| 需要解构给子组件/模板 | toRefs(state) | 保证解构后仍是响应式 |
| 需要整体替换对象 | ref | reactive 替换变量会丢代理 |
| 传给组合式函数 | ref 或 toRefs | 组合函数通常按 ref 的 .value 来读 |
再补一条隐藏坑:reactive 如果整体替换变量指向,会丢掉原来的代理。
let state = reactive({ count: 0 })
state = { count: 1 } // ❌ 变量指向了普通对象,代理断了
// ❌ 这样也不完美:新变量是响应式的,但别处还握着的旧代理已经对不上了
state = reactive({ count: 1 })
// ✅ 原地改,代理还在
Object.assign(state, { count: 1 })
// ✅ 或者一开始就用 ref,整体替换 .value
const stateRef = ref({ count: 0 })
stateRef.value = { count: 1 } // 仍然是响应式
ref 之所以更通用,是因为它把「值的引用」也包进了对象里——替换 .value 不会丢失响应式;reactive 更适合「同一个对象上改属性」,不适合「整份对象换掉」。
toRefs 的进阶用法
用法 1:把 reactive 拆成多个 ref 传给组合式函数
function useCounter(countRef) {
const double = computed(() => countRef.value * 2)
return { double }
}
const state = reactive({ count: 0 })
const { count } = toRefs(state)
const { double } = useCounter(count)
用法 2:toRef 和 toRefs 的区别
toRef(obj, 'key')—— 单个属性转 reftoRefs(obj)—— 所有属性转 ref
用法 3:toRef 可以用来读 props 的某个属性
const props = defineProps(['count'])
const countRef = toRef(props, 'count') // ✅ 保持响应式
// 直接解构 props.count 会丢响应式
flowchart TB
R[reactive state] -->|toRefs| T1["count: ref"]
R -->|toRefs| T2["name: ref"]
T1 -.->|".value 读写"| R
T2 -.->|".value 读写"| R
总结 + 面试速答
如果只记三件事:
reactive和ref不是"二选一",是前者只接对象、后者接任意值toRefs解决的不是"用哪个",而是解构后响应式会丢这个独立问题ref比reactive更通用,因为 reactive 不能整体替换、不能接基本类型
面试速答版(可以直接背):
reactive基于 Proxy,只能接受对象,返回的是代理对象;ref通过{ value }包装可以支持任意类型,所以基本类型只能用ref。
toRefs的作用是把reactive的每个属性转换成指向原对象的ref,解决解构后响应式丢失的问题。如果直接解构reactive返回的普通值,就会变成一个脱离响应式系统的普通变量。选择上:对象用
reactive更简洁(少写.value),基本类型和需要整体替换的场景用ref;团队若追求风格统一,全用ref也合理。需要解构给子组件或组合式函数时用toRefs。
参考
- Vue 3 官方文档 — 响应式基础(cn.vuejs.org/guide/essen…)
- Vue 3 官方文档 — 响应式 API:核心(cn.vuejs.org/api/reactiv…)
- Vue 3 官方文档 — 响应式 API:工具函数(cn.vuejs.org/api/reactiv…)