前言
在 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的读写拦截。
import{ref}from'vue'constcount=ref(0)// 实际结构类似于:{ value: 0 }console.log(count.value)// 0count.value++// 修改值2.2 ref 的核心特点
| 特点 | 说明 |
|---|---|
| 适用类型 | 任意类型(基本类型、对象、数组均可) |
| 访问方式 | 必须通过.value访问和修改 |
| 模板中使用 | 自动解包,无需.value |
| 重新赋值 | ✅ 可以安全地重新赋值,保持响应性 |
| 解构 | ❌ 解构会失去响应性 |
2.3 ref 的底层原理
ref的实现相对直观——它创建了一个带有.value属性的对象,通过 getter/setter 来追踪依赖和触发更新:
// 简化版的 ref 实现functionref(value){// 如果传入的是对象,内部会用 reactive 包裹if(isObject(value)){value=reactive(value)}return{getvalue(){track()// 收集依赖returnvalue},setvalue(newVal){value=newValtrigger()// 触发更新}}}关键点在于:
- 当包装的是对象时,
ref内部会使用reactive来确保对象的响应性 - 基本类型通过
.value的 getter/setter 实现响应式 - 模板中会自动解包
ref,所以不用写.value
2.4 ref 的使用示例
<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 代理,对代理对象的任何属性读写都会被拦截。
import{reactive}from'vue'conststate=reactive({count:0,user:{name:'张三',age:25}})state.count++// 直接修改,无需 .valuestate.user.name='李四'3.2 reactive 的核心特点
| 特点 | 说明 |
|---|---|
| 适用类型 | 仅对象类型(Object、Array、Map、Set 等) |
| 访问方式 | 直接访问属性,无需.value |
| 模板中使用 | 直接使用,与普通对象无异 |
| 重新赋值 | ❌ 不能直接重新赋值整个对象,否则失去响应性 |
| 解构 | ❌ 解构会丢失响应性 |
3.3 reactive 的底层原理
reactive基于 ES6 的Proxy实现。Proxy 可以拦截对目标对象的所有操作——读取、写入、删除、遍历等:
// 简化版的 reactive 实现functionreactive(target){if(typeoftarget!=='object'||target===null){returntarget}returnnewProxy(target,{get(obj,key){track(obj,key)// 收集依赖constres=obj[key]// 嵌套对象递归代理returntypeofres==='object'?reactive(res):res},set(obj,key,value){constresult=Reflect.set(obj,key,value)trigger(obj,key)// 触发更新returnresult},deleteProperty(obj,key){constresult=Reflect.deleteProperty(obj,key)trigger(obj,key)// 删除属性也能触发更新returnresult}})}Vue 2 vs Vue 3 响应式对比:
| 能力 | Vue 2 (defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 监听数组索引修改 | ❌ | ✅ |
| 监听 push/pop | ⚠️ 重写方法 | ✅ 原生支持 |
| 监听新增属性 | ❌ 需 Vue.set | ✅ 自动监听 |
| 监听删除属性 | ❌ 需 Vue.delete | ✅ 自动监听 |
| 性能 | 初始化遍历所有属性 | 惰性代理,更高效 |
3.4 reactive 的使用示例
<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 实战案例一:计数器 + 用户表单
<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都能用,但行为有差异:
<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 开发中最常见的错误之一:
// ❌ 错误:解构 reactive 对象conststate=reactive({count:0,name:'Vue'})const{count,name}=state// count 和 name 变成了普通变量!count++// 不会触发视图更新解决方案:使用toRefs将每个属性转为ref:
import{reactive,toRefs}from'vue'conststate=reactive({count:0,name:'Vue'})const{count,name}=toRefs(state)// ✅ count 和 name 都是 refcount.value++// ✅ 会触发更新6.2 ⚠️ 陷阱二:给 reactive 重新赋值
// ❌ 错误letstate=reactive({count:0})state={count:1}// state 不再是响应式对象!// ✅ 正确:修改属性,而非替换对象state.count=1// ✅ 正确:如果需要重置,使用 Object.assignObject.assign(state,{count:1,name:'Vue'})6.3 ⚠️ 陷阱三:在 reactive 中使用基本类型会报警告
// ❌ 错误:reactive 不支持基本类型constcount=reactive(0)// 控制台警告:value cannot be made reactive// ✅ 正确:基本类型用 refconstcount=ref(0)6.4 ⚠️ 陷阱四:ref 在 reactive 数组/集合中不会自动解包
在 Vue 3 中,当ref作为响应式数组或原生集合(如Map)的元素时,不会自动解包.value,需要手动访问:
constcount=ref(0)constarr=reactive([count])console.log(arr[0].value)// 需要 .value,而不是 arr[0]6.5 ⚠️ 陷阱五:忘记 .value
在<script>中操作ref时,必须使用.value:
constcount=ref(0)// ❌ 错误count++// 报错:count 是对象,不能自增// ✅ 正确count.value++而在<template>中,Vue 会自动解包,不需要.value:
<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 的关键一步。希望这篇文章能帮你彻底搞清楚这对“响应式双雄”,在实际项目中用得更加得心应手。