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。


参考

相关推荐
不可能片场1 小时前
Windows 中文传参坑 命令行乱码变量来救场
前端·electron
cidy_981 小时前
第 1 章:项目初始化与技术选型
前端
huakoh1 小时前
MCP 参数校验失败,isError 还是 InvalidParams?用三条探针定归属
前端
涛涛ing1 小时前
你的网站准备好被AI Agent阅读了吗?2026年最被忽视的前端工程问题
前端
百万蹄蹄向前冲1 小时前
妙!文档懒得写,就让AI 生成新员工学习指南
前端·后端·trae
狗头大军之江苏分军1 小时前
《潮水漫过十七岁》对她的感觉
前端·后端
狗头大军之江苏分军1 小时前
《潮水漫过十七岁》我想买相机
前端·后端
excel1 小时前
Node.js 常用反爬虫工具推荐
前端