我们来详细讲解 Vue 3 中的两个核心响应式 API:ref 和 reactive。这是理解 Vue 3 组合式 API(Composition API)的基础。
核心概念
Vue 3 的响应式系统允许我们声明数据,并在数据变化时自动更新相关的视图。ref 和 reactive 是创建这种响应式数据的两种主要方式。
1. reactive
reactive() 用于创建一个响应式的对象(包括数组和内置的集合类型,如 Map、Set)。
用法
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 25
},
hobbies: ['reading', 'gaming']
})
特点
-
针对对象:只对对象类型有效(对象、数组、Map、Set等),对原始类型(如
string,number,boolean)无效。 -
深度响应:
reactive会递归地将所有嵌套的属性也变为响应式的。上面例子中的state.user.name和state.hobbies[0]都是响应式的。 -
访问和修改:可以直接通过
.来访问和修改属性,语法与普通对象无异。// 修改值 state.count++ state.user.name = 'Bob' state.hobbies.push('hiking') // 访问值 console.log(state.count) -
注意事项:
-
不能直接替换整个对象:如果你给
reactive的变量赋予一个全新的对象,会失去响应性。// 错误!这将导致 state 失去响应性 state = reactive({ count: 10 }) // 正确的方法是逐个修改属性 Object.assign(state, { count: 10, someNewProp: 'hi' }) -
与原始对象无关:
reactive返回的是一个 Proxy 对象,它和原始对象是不相等的。const raw = {} const proxy = reactive(raw) console.log(proxy === raw) // false
-
2. ref
ref() 可以用来创建任何类型的响应式数据,包括原始类型。它通过一个 .value 属性来获取和设置值。
为什么需要 ref?
因为 reactive 只能用于对象,而原始值(如数字、字符串)需要有自己的响应式包装器,这就是 ref 的作用。
用法
import { ref } from 'vue'
const count = ref(0) // 原始类型
const user = ref({ name: 'Alice' }) // 对象类型
const list = ref([]) // 数组类型
特点
-
适用所有类型:既可以处理原始值,也可以处理对象。当处理对象时,内部会调用
reactive来深度转换。 -
通过
.value访问:你需要通过.value属性来读写ref的值。// 修改值 count.value++ user.value.name = 'Bob' // 对于对象,无需再 .value.name list.value.push('item') // 访问值 console.log(count.value) console.log(user.value.name) -
在模板中自动解包:当
ref在模板顶层(<template>)被渲染时,会自动“解包”,无需使用.value。<template> <div>{{ count }}</div> <!-- 无需 .value --> <button @click="count++">Increment</button> <!-- 无需 .value --> </template>注意:在深层嵌套的对象或数组中,在模板中不会自动解包,仍需
.value。 -
响应式替换:你可以将整个
.value替换掉,它仍然是响应式的。user.value = { name: 'Charlie', age: 30 } // 完全没问题!
ref vs reactive:如何选择?
这是一个常见的初学者问题。以下是常见的实践和推荐用法:
| 特性 | ref | reactive |
|---|---|---|
| 数据类型 | 所有类型 | 仅对象类型 |
| 访问方式 | 需要 .value | 直接访问属性 |
| 模板使用 | 自动解包(顶层) | 直接访问 |
| 重新赋值 | 可以整体替换(.value = ...) | 不能整体替换,会失去响应性 |
选择建议:
-
使用
ref的情况:- 定义原始类型的响应式数据(如
string,number,boolean)。 - 定义可能需要整体替换的响应式对象或数组。
- 在不确定使用哪个时,优先使用
ref。因为它更通用,规则更一致(总是通过.value访问),减少了“何时需要.value”的困惑。
- 定义原始类型的响应式数据(如
-
使用
reactive的情况:- 定义不需要整体替换的、结构固定的复杂对象(如表单数据、页面状态对象)。
- 当你确实想使用解构(但请注意会失去响应性,见下文)时,
reactive可以与toRefs配合。
一个重要的注意事项:解构
直接对 reactive 对象进行 ES6 解构,会破坏响应性,因为解出来的是普通变量。
const state = reactive({ count: 0 })
// 普通解构:count 不再是响应式的!
let { count } = state
count++ // 不会更新视图
// 使用 toRefs 保持响应性
import { toRefs } from 'vue'
const state = reactive({ count: 0 })
// 每个属性都被转换成了一个 ref
let { count } = toRefs(state)
count.value++ // 这样是响应式的
而 ref 本身就没有这个问题,因为它本身就是一个独立的变量。
总结
| API | 推荐场景 | 访问 | 备注 |
|---|---|---|---|
ref | 通用首选。原始值、对象、数组。尤其是需要整体替换时。 | xxx.value | 模板中顶层自动解包,无需 .value。 |
reactive | 不需要替换的复杂对象状态。 | 直接 obj.key | 不能整体替换。解构需配合 toRefs。 |
简单记忆:
- 如果是单个变量(尤其是字符串、数字、布尔值),用
ref。 - 如果是一组逻辑相关的、需要一起使用的数据集合(如表单的多个字段),可以用
reactive包装成一个对象,然后在组合式函数中返回时用toRefs转换为多个ref,以便解构后保持响应性。
在实践中,很多开发者发现全程使用 ref 反而心智负担更小,因为规则始终如一(总是用 .value),所以这也是一个非常流行和有效的选择。