一文搞懂Vue3中的ref和reactive
作者:小林猿~
引言
在 Vue3 中,响应式最常用的两个 API 就是 ref
和 reactive
。很多开发者一开始对它们的区别不够明确,看到任何状态就想用 ref
,或者对对象也习惯性用 ref
包一层,导致代码可读性、维护性下降,或者出现解构导致响应丢失、整体替换麻烦等问题。
1. 基本概念
ref:用于包装一个独立的响应式值。创建后会返回一个包含
.value
的对象,内部对.value
做响应式跟踪。模板中 Vue 会自动解包.value
,在 JS 逻辑里需显式用.value
访问或修改。import { ref } from 'vue'; const count = ref(0); count.value++; // 触发响应
reactive:用于把一个对象或数组变为响应式 Proxy。返回的就是该对象的代理,访问写法如
state.prop
,对内部嵌套对象/数组会递归转为响应式。import { reactive } from 'vue'; const state = reactive({ a: 1, b: { c: 2 } }); state.a = 3; // 触发响应 state.b.c = 5; // 嵌套也响应
关键区别
ref
更适合包装原始类型或需要整体替换的场景;.value
代表实际值。reactive
适合包装多字段对象/数组,直接访问属性更简洁。
2. 访问与解包:模板 vs JS 逻辑
模板中:Vue 会自动对
ref
进行解包。例如:<template> <div>{{ count }}</div> <!-- 如果 count = ref(0),模板会显示 0 --> <div>{{ state.a }}</div> <!-- state = reactive({ a: ... }) --> </template>
模板里使用
ref
、reactive
返回的变量都可直接写,不用加.value
。JS 逻辑里:
ref
:必须用.value
访问/赋值。reactive
:直接用state.prop
、state.prop = newValue
。
示例:
import { ref, reactive } from 'vue'; export default { setup() { const loading = ref(false); const filters = reactive({ category: '', minPrice: null, maxPrice: null }); function doSomething() { loading.value = true; // ref filters.category = 'electronics'; // reactive } return { loading, filters, doSomething }; } };
3. 场景对比:何时用 ref,何时用 reactive
下面以电商常见需求为例,了解它们的使用差异。
3.1 单值状态 vs 多字段状态
单值、标志位、计数器、页码、布尔 loading、是否收藏 等,典型用
ref
。语义上就是一个变量,访问/修改都集中在.value
,且若需要整体替换该值(如重置、切换)也很方便。const page = ref(1); const loading = ref(false); const isFavorite = ref(false); // 切换时: page.value = 1; isFavorite.value = true/false
多个相关字段聚合成一个对象,如搜索筛选条件、表单数据、购物车项集合和详情对象等,用
reactive
更直观:const filters = reactive({ keyword: '', category: '', price: { min: null, max: null } }); // 修改时: filters.keyword = 'xxx'; filters.price.min = 10
若拆成多个 ref:
const keyword = ref('')
,const category = ref('')
, …,当字段较多时不易管理;也无法一次性传递或传入 API。
3.2 整体替换 vs 逐字段更新
需要整体替换状态对象:如“加载远程购物车数据并直接赋予当前状态”“一键清空并恢复初始对象”等。用
ref
包对象更简单:const cartRef = ref({ items: [], couponCode: '', total: 0 }); // 加载后整体替换 cartRef.value = newCartObj;
若用
reactive
:const cart = reactive({ items: [], couponCode: '', total: 0 });
// 替换时不能直接 cart = newCartObj,否则不会触发响应;需要: Object.assign(cart, newCartObj); // 或手动清空 items: cart.items.splice(0), 重置其他字段 `Object.assign` 写法较繁琐,且当对象属性更新逻辑复杂时需注意字段对齐。 - **只修改某些字段/数组操作**:如在购物车中增加、减少某项数量、移除某项、更新优惠券、增加浏览次数等,多是逐字段操作,更适合 `reactive`,写法简洁: const cart = reactive({ items: [], couponCode: '', total: 0 }); function increase(idx) { cart.items[idx].quantity++; recalcTotal(); }
如果用 ref
,写成 cartRef.value.items[idx].quantity++
,多了 .value
,可读性略差。
3.3 组合场景示例
以搜索筛选和分页为例,结合 ref/reactive:
<template> <input v-model="searchQuery" @keyup.enter="doSearch" placeholder="搜索商品" /> <select v-model="filters.category"> <option value="">全部</option> <option value="electronics">电子</option> </select> <div> <button @click="prevPage" :disabled="page <= 1">上一页</button> <span>第 {{ page }} 页</span> <button @click="nextPage">下一页</button> </div> <div v-if="loading">加载中...</div> <ul v-else> <li v-for="item in list" :key="item.id">{{ item.name }}</li> </ul> </template> <script setup> import { ref, reactive } from 'vue'; const searchQuery = ref(''); const page = ref(1); const loading = ref(false); const filters = reactive({ category: '', priceRange: { min: null, max: null } }); const list = ref([]); async function fetchData() { loading.value = true; // 构造参数 const params = { q: searchQuery.value, category: filters.category, min: filters.priceRange.min, max: filters.priceRange.max, page: page.value }; // 假设调用接口返回 items、hasMore const res = await fetchProducts(params); list.value = res.items; loading.value = false; } function doSearch() { page.value = 1; fetchData(); } function prevPage() { if (page.value > 1) { page.value--; fetchData(); } } function nextPage() { page.value++; fetchData(); } </script>
searchQuery
,page
,loading
,list
独立值用ref
;filters
多字段用reactive
。两者结合,写法语义清晰、逻辑分明。
4. 解构与响应丢失:常见陷阱
reactive 解构后丧失响应
const state = reactive({ a: 1, b: 2 }); const { a, b } = state; // 这里的 a、b 都是普通值,不再是响应式。模板或 watch 无法再跟踪它们。
解决:若想解构并保持响应,可用
toRefs
:import { reactive, toRefs } from 'vue'; const state = reactive({ a: 1, b: 2 }); const { a, b } = toRefs(state); // a、b 都是 ref,保持响应
ref 解构注意
对于const count = ref(0)
,一般直接用count
;不应做const { value } = count
,因为拿到的value
是初始值,后续对count.value
修改不会更新这个解构后的value
变量,也不会触发响应。整体替换 vs 解构
如果本来想整体替换 reactive 对象,解构后再合并新对象会更复杂。一般用 ref 包对象来明确表示整体替换。
5. 深度 vs 浅层响应
- reactive 默认深度递归:内部嵌套对象/数组会在访问时或初始化时转为 Proxy。
- ref 包对象:当值是对象或数组时,Vue 内部会对其做 reactive 处理,达到深度响应。但访问时仍需
.value
。 - shallowReactive / shallowRef:在特殊场景下,如果不想对深层嵌套做自动响应,可使用浅响应 API。但多数情况下默认深度足够。
示例:若想对顶层字段变化跟踪,但不关心内部深层变化,可用 shallowReactive({ nested: { ... } })
。
6. 团队实践与语义统一
保持一致性:团队可制定简单约定,比如:
- 单值用
ref
;多字段状态用reactive
。 - 若有大量整体替换场景,将对应对象用
ref
包裹,并在注释或文档中标明“此状态将整体赋值替换”。 - 避免随意把对象都用
ref
包一层或把所有状态都放到一个大 reactive 对象,导致解构、替换、类型推断等复杂。
- 单值用
TypeScript 友好:无论 ref 还是 reactive,都有相应类型推断。可结合接口定义:
interface Cart { items: CartItem[]; couponCode: string; total: number; } const cartRef = ref<Cart>({ items: [], couponCode: '', total: 0 }); // 或 reactive: const cart = reactive<Cart>({ items: [], couponCode: '', total: 0 });
- 如果用 reactive,类型推断里访问
cart.items
等一目了然;用 ref 包对象时访问需.value.items
,类型也清晰,但习惯上需注意。
- 如果用 reactive,类型推断里访问
可读性与维护:若某状态对象很大且只在特定场景整体替换,ref 包对象能让代码一眼看出“这是一个整体”;若多个位置需单字段修改,用 reactive 写法更简洁。结合团队项目实际需求,选择更贴近业务意图的方式。
7. 性能与底层实现简述
- 底层都是 Proxy & effect:
reactive
底层用 Proxy 拦截 get/set;ref
底层实现封装了对.value
的 track/trigger,如果值是对象内部也会递归转 reactive。 - 性能差异极小:两者在常规使用下性能相当。区别主要在语义和写法。
- 初始化开销:reactive 在访问深层属性时会懒递归(或初始化时深度遍历,取决内部实现策略),而 ref 包基本类型开销更小;但在对象场景,开销差异在可接受范围。通常不必为性能过度担心,而是根据使用场景选择更清晰易维护的方式。
8. 常见误区纠正
“所有状态都用 ref”
- 虽然把对象用
ref(obj)
也能响应,但会导致 JS 逻辑里到处出现.value
,可读性差;解构/传参麻烦;整体替换与逐字段更新语义不够明确。 - 正确做法应先判断:如果只是想更新字段,推荐 reactive;若经常整体赋新值,ref 包对象可考虑。
- 虽然把对象用
“所有状态都用 reactive”
- 当只需管理单一值时,用 reactive 会将其包在对象里(如
reactive({ count: 0 })
),这写法冗余;并且整体替换需要 Object.assign,不够直观。 - 对于标志位、计数、单值 API 返回数据、Boolean 开关等,推荐用 ref。
- 当只需管理单一值时,用 reactive 会将其包在对象里(如
忽视解构导致响应丢失
- 解构 reactive 对象字段时要用
toRefs
;否则解构后的变量再修改无法触发视图更新。面试中若提到解构,需说明解决方案。
- 解构 reactive 对象字段时要用
混用场景未做区分
- 例如把整个表单数据既用 reactive 管理,也在某些地方把它赋值给 ref,再修改一半字段时容易混淆响应;要在代码风格上统一,明确何时整体替换、何时字段更新。
9. 总结
核心认识:
ref
与reactive
并非完全可互换,而是分别针对“独立值/整体替换”和“复合对象/逐字段更新”场景设计。理解两者语义、访问方式和解构注意。建议:
- 先分析业务需求:状态是单一值还是多字段聚合?是否需要整体替换或只是字段更新?是否会在多个地方解构或传递?
- 按需选择:简单标志、计数、页码、loading、Boolean、ID 等用
ref
;多个字段组合成配置、表单、购物车列表、复杂详情对象等用reactive
。 - 若有整体替换需求,也可用
ref
包对象。若用 reactive,注意用Object.assign
或手动清理以触发响应。 - 谨慎解构:了解
toRefs
用法;在 Composition API 里尽量直接操作 ref/reactive,避免无谓解构。 - 团队约定:统一风格,避免不同开发者随意选择导致代码风格混乱。
一句话概括:
“Vue3 里
ref
是给单一值或需要整体替换的状态包装响应式,reactive
是给对象/数组做深度响应,用在多字段状态更新更直观,两者语义不同、写法不同,应根据需求选用。”
到此这篇关于玩懂Vue3的ref和reactive的文章就介绍到这了,更多相关vue ref和reactive内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!