ref、reactive、toRefs 到底该用哪个?我把三个反例都写了一遍

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 不能接基本类型

js 复制代码
const count = reactive(0) // ❌ 控制台警告:value cannot be made reactive

原因上一篇讲过了:Proxy 的第一个参数必须是对象。基本类型(数字、字符串、布尔)没法被代理。

所以基本类型只能用 ref:

js 复制代码
const count = ref(0) // ✅ 包了一层 { value: 0 }

限制 2:reactive 解构后会丢响应式

这是 90% 的人踩过的坑,也是 toRefs 最常见的存在理由(后面还会看到:拆给组合式函数、读 props 也是):

js 复制代码
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[&#34;count: 0<br/>❌ 普通值,断开连接&#34;] end subgraph toRefs S2[reactive state] -->|toRefs| C2[&#34;count: ref<br/>✅ 指向原对象&#34;] end

toRefs 做的事不是"把 reactive 转成 ref",而是把 reactive 的每个属性转成一个指向原对象的 ref------你改这个 ref,改的还是原对象。

js 复制代码
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)

count.value++ // ✅ 视图会更新

三个反例

反例 1:以为 reactive 解构一定会丢响应式

js 复制代码
const state = reactive({ userInfo: { name: '' } })
const { userInfo } = state

userInfo.name = 'xxx' // ✅ 会更新

这个会更新。因为 userInfo 是一个对象的引用,解构出来的是同一个对象的引用。

但:

js 复制代码
const state = reactive({ count: 0 })
let { count } = state
count++ // ❌ 不会更新

解构基本类型会丢,解构对象引用不会。 这个差异很多人没意识到。

反例 2:ref 用在对象上,多写了一层 .value

js 复制代码
// ❌ 不是不能用,只是脚本里多一层 .value
const user = ref({ name: 'Vue', age: 3 })
user.value.name = 'React' // 只有一层 .value,再访问属性

对象直接用 reactive 更省事:

js 复制代码
// ✅
const user = reactive({ name: 'Vue', age: 3 })
user.name = 'React'

补充:模板里 ref 会自动解包,写 user.name 就行,不用写 user.value.name。多出来的 .value 主要烦在 script 里。

反例 3:return 时把 reactive 拆散了,模板丢响应式

容易踩的不是「模板要不要写 .value」(模板里 ref 会自动解包),而是 return 时把响应式拆坏了:

js 复制代码
// ❌ 展开 / 解构后再 return,拿到的是普通值
const state = reactive({ count: 0 })
return { ...state }
// 或:const { count } = state; return { count }
html 复制代码
<!-- 点按钮改 count,视图不会更新 -->
<button @click="count++">{{ count }}</button>

正确写法:把整个代理 return 出去,或用 toRefs 保住连接:

js 复制代码
// ✅ 方案 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 如果整体替换变量指向,会丢掉原来的代理。

js 复制代码
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 传给组合式函数

js 复制代码
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') ------ 单个属性转 ref
  • toRefs(obj) ------ 所有属性转 ref

用法 3:toRef 可以用来读 props 的某个属性

js 复制代码
const props = defineProps(['count'])
const countRef = toRef(props, 'count') // ✅ 保持响应式
// 直接解构 props.count 会丢响应式
flowchart TB R[reactive state] -->|toRefs| T1[&#34;count: ref&#34;] R -->|toRefs| T2[&#34;name: ref&#34;] T1 -.->|&#34;.value 读写&#34;| R T2 -.->|&#34;.value 读写&#34;| R

总结 + 面试速答

如果只记三件事:

  1. reactive 和 ref 不是"二选一",是前者只接对象、后者接任意值
  2. toRefs 解决的不是"用哪个",而是解构后响应式会丢这个独立问题
  3. ref 比 reactive 更通用,因为 reactive 不能整体替换、不能接基本类型

面试速答版(可以直接背):

reactive 基于 Proxy,只能接受对象,返回的是代理对象;ref 通过 { value } 包装可以支持任意类型,所以基本类型只能用 ref。

toRefs 的作用是把 reactive 的每个属性转换成指向原对象的 ref,解决解构后响应式丢失的问题。如果直接解构 reactive 返回的普通值,就会变成一个脱离响应式系统的普通变量。

选择上:对象用 reactive 更简洁(少写 .value),基本类型和需要整体替换的场景用 ref;团队若追求风格统一,全用 ref 也合理。需要解构给子组件或组合式函数时用 toRefs。


参考

相关推荐
疯华整猫7 小时前
被杀掉之后:我把 Android 的「唤起 + 常驻」重做了一遍
前端·面试·架构
ruanno_top8 小时前
点全选,已选的被清空了:Transfer 三态复选框的三个 bug
vue.js
时间的情敌9 小时前
Cesium 加载 3D Tiles 倾斜摄影、压平、样式设置(Vue3 实战)
vue.js·3d·cesium
IT_陈寒9 小时前
Java开发实战:从入门到精通
前端·人工智能·后端
风骏时光牛马10 小时前
生成一个AI转岗经历标题,只需要一个标题,不需要其他的任何文本。
前端
Csvn19 小时前
Monorepo
前端
Csvn19 小时前
流水线
前端
Csvn19 小时前
微前端架构
前端
Linux-lucky21 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
风骏时光牛马1 天前
生成一个移动端标题,只需要一个标题,不需要其他的任何文本。
前端