Vue 3 提供了两种定义响应式数据的主要方式:ref 和 reactive。很多开发者一开始容易陷入“该用哪个”的纠结里,其实选型规则可以归纳为一句简单的话:基本类型用 ref,引用类型随意,但推荐对象也走 ref 统一的模式。下面展开说明为什么这样选,以及需要避开的坑。
为什么基本类型必须用 ref
reactive 是基于 Proxy 实现的,而 Proxy 只能代理对象类型的数据。如果你把一个基本类型(如字符串、数字、布尔值)直接传给 reactive,它要么报错,要么无法保持响应式。因此,对于数字、字符串这类单个值,唯一的选择就是 ref。
import { ref } from 'vue'
const count = ref(0) // 正确:基本类型用 ref
const name = ref('Tom') // 正确
let countReactive
try {
countReactive = reactive(100) // 报错或无效
} catch (e) {
console.log('reactive 不能接收基本类型')
}
对象类型用 reactive 的“天然直觉”
reactive 是为对象设计的 API,它返回一个代理对象,你直接操作属性的增加、修改、删除都能自动触发响应式更新,而且使用时不需要 .value,写起来更接近原生对象的习惯。
import { reactive } from 'vue'
const user = reactive({
name: 'Alice',
age: 30
})
user.age = 31 // 视图自动更新,无需 user.value.age
user.department = 'FE' // 新增属性也支持响应式(Vue 3 的 Proxy 优势)
这种“不用写 .value”的体验,让很多开发者在处理复杂对象时首选 reactive。
为什么实际开发中 ref 更“稳”
尽管 reactive 看起来很自然,但它有几个硬伤,导致大型项目和社区最佳实践中反而更推荐统一使用 ref,即使对于对象类型。
1. 解构或替换整个对象会丢失响应式reactive 返回的响应式状态与原始对象本身绑定在一起。如果你对它进行解构、展开,或者直接重新赋值,响应式就断了。
const state = reactive({ count: 1, message: 'hello' })
// 解构丢失响应式:
const { count, message } = state
count++ // 视图不更新!
// 直接替换整个对象丢失响应式:
state = reactive({ count: 2 }) // 不行,而且这是重新赋值变量,通常不会这样写
// 正确做法:用 Object.assign 覆盖属性
Object.assign(state, { count: 2 })
而在 ref 中,响应式连接在 .value 上,你可以随意对 .value 赋值新的对象,响应式依然保持:
const state = ref({ count: 1, message: 'hello' })
state.value = { count: 2, message: 'world' } // 视图更新
const { count, message } = state.value // 解构的是普通值,不指望它保持响应式
2. reactive 不能直接替换整个值
当你把一个 API 返回的新数据直接赋予 reactive 变量时,你不能简单写 myData = newData,因为这会让 myData 指向一个新对象,失去响应式。你只能遍历赋值或 Object.assign。而 ref 只需要 myData.value = newData。
3. 组合式函数(Hooks)返回值的统一性
如果你写一个自定义 Hook,返回的是 reactive 对象,外部解构使用会失去响应式,必须要求外部了解且避免解构;返回的是 ref,则 .value 的写法只是调用侧的规则,但不会因为不当操作丢失响应式。因此,为了 Hook 的健壮性,官方推荐在 Hook 中暴露 ref。
两种“统一选型”策略
社区经过大量实践后,形成两种主流选择策略,你可以任选其一并在团队内保持一致:
策略 A:纯 ref 流(通用推荐)
- 所有响应式数据都用
ref,包括对象和数组。 .value写起来稍微啰嗦,但语义统一,不会遇到解构丢失响应式或替换整个值的问题。- 在
<script setup>中可以直接用const count = ref(0),模板里会自动解包(不用写.value),所以实际上补充的成本只在 JS 逻辑代码中。
策略 B:分类型选用流
- 基本类型用
ref,对象/数组用reactive。 - 要遵守“不直接对
reactive对象解构、不直接重新赋值”的纪律。 - 需要使用
toRefs()来安全解构,增加一些模板代码。
关键点速记
| 场景 | 推荐 | 原因 |
|------|------|------|
| 数字、字符串、布尔值 | ref | reactive 不支持基本类型 |
| 单个数组或对象,且不会解构 | ref 或 reactive 均可 | ref 更灵活,reactive 少写 .value |
| 需要安全解构或整体替换 | ref | .value 赋值天然响应,避免丢失 |
| 自定义 Hook 返回 | ref | 保证外部使用安全 |
| 表单、配置等局部状态 | reactive 也可以 | 作用域小,不会到处解构,直观 |
一句话总结:Vue 官方提供两种 API 是为了灵活,而不是制造困扰。最简单的选择就是所有响应式数据都用 ref,这几乎可以避免所有关于响应式丢失的坑,是整个团队最容易统一、代码审查成本最低的方案。