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

153 阅读5分钟

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

摘要:ref 和 reactive 不是"哪个更好",是设计前提不同。三个反例,把 toRefs 为什么存在讲透。


上一篇《我重写了 Vue3 的响应式》里,我讲了 ref 为什么必须 .value——那是从"语言层面"解释的。这篇换个角度:实际项目里,ref、reactive、toRefs 到底该怎么选。

image.png


先给结论

这三个 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[&#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,改的还是原对象。

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++ // ❌ 不会更新

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

image.png

反例 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 里。

image.png

反例 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)

到底该用哪个?

场景推荐理由
基本类型(数字、字符串、布尔)refreactive 接不了
纯对象、表单数据reactive 或 ref前者少写 .value;团队统一用 ref 也行
需要解构给子组件/模板toRefs(state)保证解构后仍是响应式
需要整体替换对象refreactive 替换变量会丢代理
传给组合式函数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') —— 单个属性转 ref
  • toRefs(obj) —— 所有属性转 ref

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

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。


参考