前言
在 Vue 3 的 Composition API 中,ref 和 reactive 是最核心的两个响应式 API。几乎每个 Vue 3 组件都离不开它们。然而,很多开发者在实际使用中仍然困惑:
ref和reactive到底有什么区别?- 为什么
ref要.value,而reactive不用? - 到底什么时候用
ref,什么时候用reactive? - 为什么解构
reactive会丢失响应性?
今天这篇文章,我将从为什么需要它们 开始,到使用方式 、底层原理 、如何选择 ,再到注意事项 ,带你一次性彻底搞懂 ref 和 reactive。
一、为什么需要 ref 和 reactive?
在 Vue 3 之前,Vue 2 的响应式系统基于 Object.defineProperty 实现。这种方式有两个明显的局限:
- 无法监听对象新增/删除属性 :必须使用
Vue.set/Vue.delete - 无法直接监听数组索引修改 :
arr[0] = x不会触发更新
更重要的是,在 Composition API 中,我们需要一种方式让普通的 JavaScript 变量变成响应式的。当数据变化时,依赖这些数据的 UI 能够自动更新。
ref 和 reactive 就是为此而生的两个核心 API。它们都能让数据变得"响应式",但设计哲学和使用方式截然不同。
二、ref:值级别的响应式容器
2.1 什么是 ref?
ref 用于创建一个响应式的数据引用 。它本质上是一个包装对象 ------内部包含一个 .value 属性,通过 getter/setter 实现对 .value 的读写拦截。
javascript
import { ref } from 'vue'
const count = ref(0)
// 实际结构类似于:{ value: 0 }
console.log(count.value) // 0
count.value++ // 修改值
2.2 ref 的核心特点
| 特点 | 说明 |
|---|---|
| 适用类型 | 任意类型(基本类型、对象、数组均可) |
| 访问方式 | 必须通过 .value 访问和修改 |
| 模板中使用 | 自动解包,无需 .value |
| 重新赋值 | ✅ 可以安全地重新赋值,保持响应性 |
| 解构 | ❌ 解构会失去响应性 |
2.3 ref 的底层原理
ref 的实现相对直观------它创建了一个带有 .value 属性的对象,通过 getter/setter 来追踪依赖和触发更新:
javascript
// 简化版的 ref 实现
function ref(value) {
// 如果传入的是对象,内部会用 reactive 包裹
if (isObject(value)) {
value = reactive(value)
}
return {
get value() {
track() // 收集依赖
return value
},
set value(newVal) {
value = newVal
trigger() // 触发更新
}
}
}
关键点在于:
- 当包装的是对象 时,
ref内部会使用reactive来确保对象的响应性 - 基本类型通过
.value的 getter/setter 实现响应式 - 模板中会自动解包
ref,所以不用写.value
2.4 ref 的使用示例
vue
<script setup>
import { ref } from 'vue'
// 基本类型
const count = ref(0)
const message = ref('Hello')
const isLoading = ref(false)
// 对象类型(ref 也可以包裹对象)
const user = ref({ name: '张三', age: 25 })
// 修改基本类型
count.value++
message.value = 'World'
// 修改对象(需要 .value)
user.value.name = '李四'
// 重新赋值整个对象------✅ 保持响应性
user.value = { name: '王五', age: 30 }
</script>
<template>
<!-- 模板中自动解包,不需要 .value -->
<p>{{ count }}</p>
<p>{{ message }}</p>
<p>{{ user.name }}</p>
</template>
三、reactive:结构级别的响应式代理
3.1 什么是 reactive?
reactive 用于将对象(包括数组、Map、Set 等) 整体转换为响应式数据。它返回的是原对象的 Proxy 代理,对代理对象的任何属性读写都会被拦截。
javascript
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: '张三', age: 25 }
})
state.count++ // 直接修改,无需 .value
state.user.name = '李四'
3.2 reactive 的核心特点
| 特点 | 说明 |
|---|---|
| 适用类型 | 仅对象类型(Object、Array、Map、Set 等) |
| 访问方式 | 直接访问属性,无需 .value |
| 模板中使用 | 直接使用,与普通对象无异 |
| 重新赋值 | ❌ 不能直接重新赋值整个对象,否则失去响应性 |
| 解构 | ❌ 解构会丢失响应性 |
3.3 reactive 的底层原理
reactive 基于 ES6 的 Proxy 实现。Proxy 可以拦截对目标对象的所有操作------读取、写入、删除、遍历等:
javascript
// 简化版的 reactive 实现
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target
}
return new Proxy(target, {
get(obj, key) {
track(obj, key) // 收集依赖
const res = obj[key]
// 嵌套对象递归代理
return typeof res === 'object' ? reactive(res) : res
},
set(obj, key, value) {
const result = Reflect.set(obj, key, value)
trigger(obj, key) // 触发更新
return result
},
deleteProperty(obj, key) {
const result = Reflect.deleteProperty(obj, key)
trigger(obj, key) // 删除属性也能触发更新
return result
}
})
}
Vue 2 vs Vue 3 响应式对比:
| 能力 | Vue 2 (defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 监听数组索引修改 | ❌ | ✅ |
| 监听 push/pop | ⚠️ 重写方法 | ✅ 原生支持 |
| 监听新增属性 | ❌ 需 Vue.set | ✅ 自动监听 |
| 监听删除属性 | ❌ 需 Vue.delete | ✅ 自动监听 |
| 性能 | 初始化遍历所有属性 | 惰性代理,更高效 |
3.4 reactive 的使用示例
vue
<script setup>
import { reactive } from 'vue'
// 定义响应式对象
const form = reactive({
username: '',
email: '',
password: '',
age: 0
})
// 定义响应式数组
const todoList = reactive([
{ id: 1, text: '学习 Vue 3', done: false },
{ id: 2, text: '写一篇博客', done: false }
])
// 修改属性------直接操作,无需 .value
form.username = 'andy'
form.age = 25
// 修改数组------直接操作
todoList[0].done = true
todoList.push({ id: 3, text: '分享给朋友', done: false })
// ⚠️ 错误:重新赋值整个对象会丢失响应性
// form = { username: 'new', email: 'new@email.com' } // ❌ 不要这样做!
</script>
<template>
<input v-model="form.username" />
<input v-model="form.email" />
<ul>
<li v-for="item in todoList" :key="item.id">
{{ item.text }}
</li>
</ul>
</template>
四、ref vs reactive:全景对比
| 对比维度 | ref |
reactive |
|---|---|---|
| 适用类型 | 任意类型 | 仅对象(Object/Array/Map/Set) |
| 访问方式 | .value |
直接访问属性 |
| 模板中使用 | 自动解包,无需 .value |
直接使用 |
| 重新赋值 | ✅ 可以安全重新赋值 | ❌ 重新赋值会丢失响应性 |
| 解构 | ❌ 解构后失去响应性 | ❌ 解构后失去响应性 |
| 底层实现 | 包装对象 + .value 的 getter/setter |
Proxy 代理整个对象 |
| 嵌套对象 | 内部自动调用 reactive |
深度代理 |
| 响应式级别 | 值级别 | 结构级别 |
五、真实项目实战:如何选择?
5.1 选择策略
根据官方推荐和社区最佳实践:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 基本类型(number/string/boolean) | ref |
reactive 不支持基本类型 |
| 单个独立值(计数、开关、输入框) | ref |
结构简单,语义清晰 |
| 表单数据(多个关联字段) | reactive |
属性直接访问,代码简洁 |
| 复杂对象/配置项 | reactive |
保持结构完整性 |
| 组合式函数返回值 | ref |
更灵活,类型统一 |
| 需要整体替换的数据 | ref |
reactive 重新赋值会丢失响应性 |
| 不确定用哪个 | ref |
更通用,更灵活 |
官方倾向 :Vue 官方文档强烈推荐使用
ref()作为声明响应式状态的主要 API。非必要情况下最好避免使用reactive。但在实际开发中,两者各有适用场景,应根据具体情况选择。
5.2 实战案例一:计数器 + 用户表单
vue
<script setup>
import { ref, reactive } from 'vue'
// ✅ 场景1:基本类型 → 用 ref
const count = ref(0)
const isLoading = ref(false)
const increment = () => {
count.value++
}
// ✅ 场景2:表单数据(关联性强)→ 用 reactive
const form = reactive({
name: '',
email: '',
phone: '',
age: 0
})
const submitForm = () => {
console.log('提交:', form)
}
// ✅ 场景3:混合使用
const userInfo = reactive({
id: 1,
name: '张三',
// 独立的状态用 ref
isEditing: ref(false),
permissions: ref(['read', 'write'])
})
</script>
5.3 实战案例二:列表管理(数组场景)
对于数组,ref 和 reactive 都能用,但行为有差异:
vue
<script setup>
import { ref, reactive } from 'vue'
// 方式一:用 ref 包裹数组
const list1 = ref([1, 2, 3])
// 修改:需要 .value
list1.value.push(4)
list1.value = [1, 2, 3, 4, 5] // ✅ 整体替换也没问题
// 方式二:用 reactive 包裹数组
const list2 = reactive([1, 2, 3])
// 修改:直接操作
list2.push(4)
// ⚠️ 不能整体替换
// list2 = [1, 2, 3, 4, 5] // ❌ 会丢失响应性
// 正确做法:清空后重新填充
list2.length = 0
list2.push(1, 2, 3, 4, 5)
</script>
建议 :如果数组需要整体替换(如从接口重新获取数据),用 ref 更方便。
六、常见陷阱与注意事项
6.1 ⚠️ 陷阱一:解构 reactive 会丢失响应性
这是 Vue 3 开发中最常见的错误之一:
javascript
// ❌ 错误:解构 reactive 对象
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // count 和 name 变成了普通变量!
count++ // 不会触发视图更新
解决方案 :使用 toRefs 将每个属性转为 ref:
javascript
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state) // ✅ count 和 name 都是 ref
count.value++ // ✅ 会触发更新
6.2 ⚠️ 陷阱二:给 reactive 重新赋值
javascript
// ❌ 错误
let state = reactive({ count: 0 })
state = { count: 1 } // state 不再是响应式对象!
// ✅ 正确:修改属性,而非替换对象
state.count = 1
// ✅ 正确:如果需要重置,使用 Object.assign
Object.assign(state, { count: 1, name: 'Vue' })
6.3 ⚠️ 陷阱三:在 reactive 中使用基本类型会报警告
javascript
// ❌ 错误:reactive 不支持基本类型
const count = reactive(0) // 控制台警告:value cannot be made reactive
// ✅ 正确:基本类型用 ref
const count = ref(0)
6.4 ⚠️ 陷阱四:ref 在 reactive 数组/集合中不会自动解包
在 Vue 3 中,当 ref 作为响应式数组或原生集合(如 Map)的元素时,不会自动解包 .value,需要手动访问:
javascript
const count = ref(0)
const arr = reactive([count])
console.log(arr[0].value) // 需要 .value,而不是 arr[0]
6.5 ⚠️ 陷阱五:忘记 .value
在 <script> 中操作 ref 时,必须使用 .value:
javascript
const count = ref(0)
// ❌ 错误
count++ // 报错:count 是对象,不能自增
// ✅ 正确
count.value++
而在 <template> 中,Vue 会自动解包,不需要 .value:
vue
<template>
<p>{{ count }}</p> <!-- ✅ 不需要 .value -->
</template>
七、总结
| 维度 | ref |
reactive |
|---|---|---|
| 核心思想 | 值级别的响应式容器 | 结构级别的响应式代理 |
| 适用场景 | 基本类型、需要整体替换的数据、不确定的场景 | 复杂对象、表单状态、关联性强的数据结构 |
| 优点 | 灵活通用、可安全重新赋值、API 一致 | 访问无需 .value、代码更简洁、嵌套自动响应 |
| 缺点 | 需要 .value 略显繁琐 |
不能重新赋值、解构会丢失响应性 |
最终建议:
- 新项目优先用
ref:更通用、更灵活、不容易踩坑 - 关联性强的对象用
reactive:表单、配置项等场景,代码更简洁 - 不确定时用
ref:它几乎可以覆盖所有场景 - 牢记
.value:在<script>中操作ref时不要忘记 - 不要解构
reactive:如需解构,使用toRefs
理解 ref 和 reactive 的区别与联系,是掌握 Vue 3 Composition API 的关键一步。希望这篇文章能帮你彻底搞清楚这对"响应式双雄",在实际项目中用得更加得心应手。