Vue3 Composition API中ref和reactive该怎么选
Vue3 引入 Composition API 后,响应式系统的构建方式发生了变化。核心的选择难题集中在 ref 和 reactive 两个 API 上。这两者虽然功能类似,但在使用场景和代码风格上有着本质区别。选择正确的 API 能避免很多“失去响应性”的常见 bug。
一、 理解核心区别
在开始选择之前,必须先搞清楚它们底层的运作逻辑。
ref:主要用于包装基本数据类型(如 string、number、boolean)。它在内部将值包装在一个对象中,通过 .value 属性访问。
reactive:用于创建深层响应式对象。它直接返回一个响应式的 Proxy 对象,不需要 .value,但只能用于对象类型(Object、Array、Map、Set 等)。
二、 严格遵循的选择标准
按照以下三个步骤进行判断,可以覆盖 99% 的开发场景。
1. 第一步:检查数据类型
判断你要定义的数据是基本类型还是对象类型。
-
如果是
string、number、boolean、null或undefined。- 强制使用
ref。 reactive无法处理这些类型,不会产生任何响应式效果。
- 强制使用
-
如果是
Object或Array。- 进入下一步判断。
2. 第二步:检查是否需要整体替换
这是最容易踩坑的地方。当你打算将来把整个对象赋值给这个变量(例如重新从 API 获取数据覆盖旧数据)时。
-
如果你需要整体赋值操作。
- 优先使用
ref。 ref包装对象后,可以直接修改.value指向新的内存地址。reactive是一个 Proxy,如果直接赋值(如userState = newUser),会切断响应性连接。
// 正确示例:使用 ref 进行整体替换 let user = ref({ name: 'Alice' }) // ✅ 获取新数据后直接覆盖 user.value = await fetchUser() // 错误示例:使用 reactive 尝试整体替换 let user = reactive({ name: 'Alice' }) // ❌ 这样做会失去响应性 user = await fetchUser() - 优先使用
3. 第三步:检查解构与模板使用习惯
如果你需要将对象的属性解构出来单独使用,或者在模板中不想总是看到 .value。
-
如果你喜欢解构赋值。
reactive解构后会丢失响应性。- 若要解构,必须配合
toRefs使用。或者直接使用ref定义每一个属性。 - 建议:如果只是简单的一层对象,直接用
ref定义属性,或者对reactive对象使用toRefs。
import { toRefs } from 'vue' const state = reactive({ count: 0, name: 'Vue' }) // ✅ 使用 toRefs 保持响应性 const { count, name } = toRefs(state) -
如果你在模板中追求极致的语法简洁。
- 在模板中,
ref会自动解包(不需要写.value),而reactive直接访问属性。 - 这一点上两者在模板中的体验几乎一致,但在 JS 逻辑中
ref需要.value。
- 在模板中,
三、 决策流程图
当你在代码中犹豫不决时,按照以下流程进行决策。
String/Number/Boolean?"} B -- 是 --> C["必须使用 ref"] B -- 否 --> D{"需要整体替换
对象吗?"} D -- 是 --> C D -- 否 --> E{"是否需要解构
或频繁传递属性?"} E -- 是 --> F["使用 reactive + toRefs"] E -- 否 --> G["使用 reactive 或 ref 均可"] G --> H{"团队代码风格偏好"} H -- 统一风格 --> C H -- 统一风格 --> F
四、 常见陷阱与解决方案
在使用过程中,有两个非常典型的错误场景,必须通过特定方式避免。
陷阱 1:试图给 reactive 属性赋值新对象
当你有一个 reactive 定义的表单对象,提交后想清空它。
const form = reactive({
username: '',
password: ''
})
// ❌ 错误做法:直接赋值一个新的空对象,会导致响应性丢失
form = { username: '', password: '' }
// ✅ 正确做法:遍历属性逐个清空,或者使用 Object.assign
Object.assign(form, { username: '', password: '' })
陷阱 2:数组在 reactive 中的索引赋值
直接通过索引修改数组元素在 Vue3 中是响应式的(不同于 Vue2),但如果配合解构使用则需小心。
const list = reactive([1, 2, 3])
// ✅ 直接通过索引赋值(Vue3 中是响应式的)
list[0] = 99
// ✅ 使用数组的修改方法
list.push(4)
list.splice(0, 1)
五、 最佳实践总结表
为了方便记忆,下表总结了两者的核心差异与适用场景。
| 特性维度 | ref | reactive |
|---|---|---|
| 适用数据类型 | 基本类型、对象 | 仅对象/数组 |
| 访问方式 (JS中) | 需要 .value |
直接访问属性 |
| 访问方式 (模板中) | 自动解包,直接访问 | 直接访问属性 |
| 整体替换对象 | ✅ 支持 | ❌ 不支持(会断开引用) |
| 解构赋值 | ✅ 解构后需用 .value,保留响应性 |
❌ 解构后丢失响应性(需 toRefs) |
| 推荐场景 | 单一基本值、需要整体替换的对象 | 深层嵌套且结构固定的复杂对象 |
| TypeScript 支持 | 需要泛型指定类型 | 推断能力强,通常无需泛型 |
六、 实际代码模板
根据上述规则,以下是一个标准的组件状态定义模板。
import { ref, reactive, computed, toRefs } from 'vue'
export default {
setup() {
// 场景 1:基本类型,使用 ref
const count = ref(0)
// 场景 2:简单的计数器逻辑
const increment = () => {
count.value++
}
// 场景 3:复杂深层对象,结构固定,使用 reactive
const userState = reactive({
profile: {
name: 'User',
age: 20,
address: {
city: 'Beijing'
}
},
isLoggedIn: false
})
// 场景 4:需要从 API 获取数据并整体替换,使用 ref
const userData = ref(null)
const fetchUserData = async () => {
const res = await api.getUser()
// ✅ 直接替换
userData.value = res.data
}
// 场景 5:将 reactive 对象的属性解构暴露给模板
// 注意:toRefs 只能用于响应式对象,不能用于普通对象
const { profile, isLoggedIn } = toRefs(userState)
return {
count,
increment,
// 返回解构后的属性,在模板中可以直接用 profile.name
profile,
isLoggedIn,
userData
}
}
}
最终建议:如果你的团队没有强制的代码规范,优先使用 ref。虽然写 .value 稍显繁琐,但它的心智负担最小,能处理所有类型的替换问题,且在解构时配合 toRefs 非常灵活。reactive 则最适合用于那些一旦定义就很少改变整体结构,只是频繁修改内部属性的复杂状态树。

暂无评论,快来抢沙发吧!