在Vue3的响应式系统中,ref
和reactive
是创建响应式数据的核心API,但许多开发者在实际开发中常困惑于"何时用ref
,何时用reactive
"。本文结合真实开发场景,通过完整代码示例拆解两者的适用场景、核心差异及避坑要点,帮助开发者高效选型。
目录
- 核心概念快速回顾
- 分场景实战:ref与reactive的正确选型
-
- 场景1:基础数据类型(数字/字符串/布尔)
- 场景2:复杂对象(多字段关联数据)
- 场景3:数组/列表数据(增删改操作)
- 场景4:需整体替换的数据(接口刷新场景)
- 场景5:解构响应式数据(保持响应性)
- 场景选择指南表(速查版)
- 常见问题答疑(避坑要点)
- 总结
1. 核心概念快速回顾
在进入场景前,先明确ref
和reactive
的本质差异,为后续选型打基础:
|-----------|-----------------|-------------------|
| 特性 | ref | reactive |
| 本质 | 响应式"容器"(包装任意类型) | 响应式"代理"(仅代理对象/数组) |
| 访问方式(脚本中) | 需通过.value
| 直接访问属性(无.value
) |
| 整体替换支持 | 支持(替换后仍保持响应性) | 不支持(替换会丢失响应性) |
| 适用类型 | 所有类型(优先基本类型) | 仅对象/数组 |
2. 分场景实战:ref与reactive的正确选型
以下场景均基于Vue3的<script setup>
语法(目前主流开发模式),代码可直接复制运行。
场景1:基础数据类型(数字/字符串/布尔)
适用场景 :存储单个简单值,如计数器、开关状态、输入框文本、标题文本等。
推荐API :ref
(唯一能响应式包装基本类型的API)
代码示例:计数器与开关状态
<template>
<div class="basic-type-demo">
<h3>计数器:{{ count }}</h3>
<button @click="increment">+1</button>
<button @click="reset">重置</button>
<div class="toggle-demo" style="margin-top: 20px;">
<p>是否显示详情:{{ isShowDetail }}</p>
<button @click="toggleShow">{{ isShowDetail ? '隐藏' : '显示' }}</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 1. 数字类型:计数器
const count = ref(0) // 初始值0,包装为ref对象
const increment = () => {
count.value++ // 脚本中必须通过.value修改
}
const reset = () => {
count.value = 0 // 直接替换值,仍保持响应性
}
// 2. 布尔类型:开关状态
const isShowDetail = ref(true)
const toggleShow = () => {
isShowDetail.value = !isShowDetail.value // 切换布尔值
}
</script>
为什么不用reactive
?
reactive
无法直接包装基本类型(如reactive(0)
会报错),若强行用对象包裹(如reactive({ count: 0 })
),会多一层不必要的嵌套,修改时需写obj.count++
,不如ref
的.value
简洁。
场景2:复杂对象(多字段关联数据)
适用场景 :存储多字段关联的数据,如用户信息、商品详情、表单配置、页面状态等。
推荐API :reactive
(直接操作属性,无需.value
,代码更简洁)
代码示例:用户信息管理
<template>
<div class="complex-object-demo">
<h3>用户信息卡片</h3>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<p>地址:{{ user.address.city }}-{{ user.address.district }}</p>
<button @click="growUp" style="margin-top: 10px;">年龄+1</button>
<button @click="updateAddress" style="margin-left: 10px;">更新地址</button>
</div>
</template>
<script setup>
import { reactive } from 'vue'
// 复杂对象:多字段+嵌套结构
const user = reactive({
name: '张三',
age: 24,
address: {
city: '上海',
district: '浦东新区'
}
})
// 直接修改属性(无.value)
const growUp = () => {
user.age++ // 直接操作嵌套属性
}
// 修改嵌套对象
const updateAddress = () => {
user.address = {
city: '北京',
district: '朝阳区'
}
}
</script>
为什么不用ref
?
若用ref
包装对象(如const user = ref({ name: '张三' })
),修改属性时需写user.value.name = '李四'
,比reactive
的user.name
多一层.value
,嵌套越深越繁琐。
场景3:数组/列表数据(增删改操作)
适用场景 :处理列表类数据,如待办事项、商品列表、表格数据,需频繁执行push
/splice
/filter
等操作。
推荐API :reactive
(原生数组方法直接用,无需.value
)
代码示例:待办列表(增删功能)
<template>
<div class="array-demo">
<h3>待办列表</h3>
<!-- 新增待办输入框 -->
<input
v-model="newTodoText"
placeholder="请输入待办"
@keyup.enter="addTodo"
>
<button @click="addTodo">添加</button>
<!-- 待办列表渲染 -->
<ul style="list-style: none; padding: 0; margin-top: 10px;">
<li v-for="(todo, index) in todoList" :key="index" style="margin: 5px 0;">
{{ todo }}
<button @click="deleteTodo(index)" style="margin-left: 10px; color: red;">
删除
</button>
</li>
</ul>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
// 数组用reactive:直接调用数组方法
const todoList = reactive(['学习Vue3响应式', '写技术博客'])
// 输入框文本用ref(基本类型)
const newTodoText = ref('')
// 新增待办
const addTodo = () => {
if (newTodoText.value.trim()) {
todoList.push(newTodoText.value) // 直接用push,无.value
newTodoText.value = '' // 清空输入框
}
}
// 删除待办
const deleteTodo = (index) => {
todoList.splice(index, 1) // 直接用splice
}
</script>
为什么不用ref
?
若用ref
包装数组(如const todoList = ref(['a', 'b'])
),执行数组方法时需写todoList.value.push(...)
,比reactive
的todoList.push(...)
多一层.value
,不够直观。
场景4:需整体替换的数据(接口刷新场景)
适用场景 :数据需被整体覆盖,如接口请求后用新数据替换旧数据、切换Tab时刷新列表。
推荐API :ref
(reactive
整体替换会丢失响应性)
代码示例:商品数据接口刷新
<template>
<div class="replace-data-demo">
<h3>商品详情</h3>
<p>名称:{{ product.name }}</p>
<p>价格:{{ product.price }}元</p>
<p>库存:{{ product.stock }}件</p>
<button @click="fetchNewProduct" style="margin-top: 10px;">
加载新商品
</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 用ref包装对象:支持整体替换
const product = ref({
name: '旧款手机',
price: 1999,
stock: 50
})
// 模拟接口请求:整体替换数据
const fetchNewProduct = () => {
// 模拟接口返回新数据
const newProductData = {
name: '新款手机',
price: 2999,
stock: 100
}
// 整体替换ref的值,仍保持响应性
product.value = newProductData
}
</script>
为什么不用reactive
?
若用reactive
(如const product = reactive({ name: '旧款手机' })
),直接替换整个对象会丢失响应性:
// 错误示例:reactive直接替换会丢失响应性
product = { name: '新款手机' } // 此时product不再是响应式代理
场景5:解构响应式数据(保持响应性)
适用场景 :需要解构响应式对象的属性(如组件传参、拆分逻辑),且解构后仍需保持响应性。
推荐方案 :reactive + toRefs
(toRefs
将reactive
属性转为ref
)
代码示例:解构用户信息并保持响应性
<template>
<div class="destructure-demo">
<h3>解构后的用户信息</h3>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<button @click="age++">年龄+1</button>
</div>
</template>
<script setup>
import { reactive, toRefs } from 'vue'
// 1. 用reactive定义响应式对象
const user = reactive({
name: '李四',
age: 28,
gender: '男'
})
// 2. 用toRefs解构:将属性转为ref对象
const { name, age } = toRefs(user)
// 此时name、age是ref对象,脚本中修改需用.age.value,模板中自动解包
// 3. 修改解构后的属性(仍保持响应性)
// age.value++ // 脚本中修改方式
</script>
避坑提示:直接解构reactive
会丢失响应性
若不使用toRefs
,直接解构reactive
对象,得到的属性是普通值,修改后不会触发视图更新:
// 错误示例:直接解构reactive会丢失响应性
const { name } = user
name = '王五' // 无响应性,视图不更新
3. 场景选择指南表(速查版)
|---------------|-------------------|------------------------|---------------------------|
| 场景描述 | 推荐API/方案 | 核心原因 | 避坑提示 |
| 基本类型(数字/字符串等) | ref | 唯一支持基本类型的响应式API | 脚本中必须用.value
修改 |
| 复杂对象(多字段) | reactive | 直接操作属性,无.value
,代码简洁 | 不能整体替换对象 |
| 数组/列表(增删改) | reactive | 原生数组方法(push/splice)直接用 | 避免用ref
包装数组(多一层.value
) |
| 需整体替换的数据 | ref | reactive整体替换会丢失响应性 | 替换时直接修改.value
|
| 解构后需保持响应性 | reactive + toRefs | toRefs将属性转为ref,保持响应性 | 不可直接解构reactive |
4. 常见问题答疑(避坑要点)
Q1:ref能包装对象吗?
能。ref
包装对象时,内部会自动调用reactive
生成代理,但访问属性需用.value
(如user.value.name
),不如reactive
直接。
Q2:reactive数组如何清空?
直接用array.length = 0
或array.splice(0, array.length)
,无需整体替换:
const todoList = reactive(['a', 'b'])
todoList.length = 0 // 正确:清空数组且保持响应性
Q3:ref解构后需要toRefs
吗?
不需要。ref
本身是容器,解构单个ref
对象(如const { value: count } = ref(0)
)无意义,直接使用count.value
即可。toRefs
仅用于reactive
对象的解构。
Q4:组件传参时,ref和reactive怎么选?
- 传基本类型:用
ref
,子组件接收后直接用.value
访问; - 传对象/数组:用
reactive + toRefs
,子组件接收ref
类型的属性,避免直接传reactive
对象(防止子组件意外修改父组件数据)。
5. 总结
ref
和reactive
没有绝对的"优劣",核心是按场景选型:
- 优先用
ref
:基本类型、需整体替换的数据; - 优先用
reactive
:复杂对象、数组列表; - 解构需响应性:搭配
toRefs
。
掌握两者的本质差异和场景边界,能有效避免"响应性丢失"等问题,提升Vue3开发效率。建议结合本文代码示例动手实践,加深理解!
#Vue3 #ref与reactive #响应式系统 #前端开发 #Vue实战