们聊透了 Proxy + effect + track/trigger 的核心流程,搞懂了 Vue3 响应式的“底层魔法”。但实际开发中,咱们很少直接写 Proxy,更多是用 ref 和 reactive 这两个“封装好的工具”。
很多新手甚至中级程序员,都会被这两个 API 搞懵:什么时候用 ref?什么时候用 reactive?为啥有时候改了数据,页面不更新(响应式丢失)?还有 shallowRef、shallowReactive 这些带“shallow”的,到底该怎么用?
今天咱们就从“基础区别”入手,由浅入深拆解,结合实际开发场景,把这些问题讲透——全程无晦涩概念,有案例、有避坑,看完不仅能分清用法,还能避开80%的响应式踩坑场景,进阶成“响应式高手”。
先定个小目标:看完这篇,你能精准判断“ref 和 reactive 该用哪个”,能快速定位并解决“响应式丢失”问题,还能灵活运用 shallow* 系列 API 优化性能。话不多说,开整!
一、基础入门:ref 和 reactive 核心区别(由浅入深,先懂表面)
首先明确一个前提:ref 和 reactive 本质都是基于 Proxy + effect 实现的响应式,没有“谁更高级”一说,只有“适用场景不同”。咱们先从最直观的3个区别入手,新手先记牢这几点,就能应对80%的基础场景。
1. 适用数据类型不同(最核心区别)
这是两者最本质的区别,也是新手最容易选错的地方,用一句话总结:
ref:用于包装“基本类型数据”,也能包装引用类型数据(String、Number、Boolean、Undefined、Null、Symbol);
reactive:只能用于包装“引用类型数据” (Object、Array、Map、Set 等),不能包装基本类型。
举个直观的例子,一看就懂:
ts
体验AI代码助手
代码解读
复制代码
// 1. ref 包装基本类型(正确用法)
const count = ref(0); // ✅ 正确,count 是响应式的count.value = 1; // 修改时需要 .value// ref 也能包装引用类型(可行,但不推荐)
const user = ref({ name: "张三", age: 20 }); // ✅ 可行,但不如 reactive 直观user.value.name = "李四"; // 修改属性时,也需要 .value// 2. reactive 包装引用类型(正确用法)
const user = reactive({ name: "张三", age: 20 }); // ✅ 正确,user 是响应式的user.name = "李四"; // 修改时直接操作属性,无需 .value// reactive 包装基本类型(错误用法,无响应式)
let count = reactive(0); // ❌ 错误,基本类型无法被 reactive 代理,count 不是响应式的count = 1; // 修改后,页面不会更新这里有个新手必踩的坑:reactive 包装基本类型时,不会报错,但完全没有响应式效果。因为 reactive 底层依赖 Proxy,而 Proxy 只能代理对象/数组,无法代理基本类型,所以会直接返回原数据,失去响应式能力。
2. 访问/修改方式不同
这是由两者的包装逻辑决定的,也是日常开发中最容易混淆的细节:
ref 包装的数据:无论包装的是基本类型还是引用类型,访问和修改时都需要通过 .value(模板中使用时,Vue 会自动解包,无需写 .value);
reactive 包装的数据:直接访问和修改属性即可,无需 .value,和操作普通对象一样直观。
补充一个模板解包的细节(新手必看):
ts
体验AI代码助手
代码解读
复制代码// 模板中使用 ref,无需 .value(Vue 自动解包)<template> <div>{{ count }}</div> // ✅ 正确,无需 count.value</template>// 脚本中必须用 .value<script setup>const count = ref(0);console.log(count.value); // ✅ 正确,必须 .valuecount.value = 1; // ✅ 正确,必须 .value</script>// reactive 无论在脚本还是模板,都无需 .value<template>
<div>{{ user.name }}</div> // ✅ 正确</template><script setup>const user = reactive({ name: "张三" });
user.name = "李四"; // ✅ 正确</script>3. 响应式原理的细微差异
前面咱们讲过,两者本质都是基于 Proxy + effect,但包装逻辑不同,导致原理上有细微差异,新手可以先了解,进阶时再深入:
ref:底层会创建一个“包装对象”(类似 { value: 原始数据 }),然后对这个包装对象使用 Proxy 代理,监听 value 的变化——所以修改时必须操作 .value,本质是监听包装对象的 value 属性;
reactive:直接对原始引用类型数据进行 Proxy 代理,监听对象的所有属性变化,所以无需 .value,直接操作属性即可触发响应。
简单总结:ref 是“间接代理”(代理包装对象),reactive 是“直接代理”(代理原始对象)。
二、进阶拆解:ref 和 reactive 深层区别(懂底层,不踩坑)
看完基础区别,咱们再往深走一步——很多时候,新手选不对 ref 和 reactive,不是不懂基础用法,而是不懂深层差异带来的场景适配问题。这部分重点讲2个深层区别,帮你精准选型。
1. 赋值行为的差异(关键!响应式丢失的伏笔)
这是最容易被忽略,但最影响开发的区别:ref 支持“直接赋值”,reactive 不支持“直接赋值”(赋值会导致响应式丢失)。
举个例子,对比明显:
ts
体验AI代码助手
代码解读
复制代码
// 1. ref 直接赋值(✅ 响应式保留)
const count = ref(0);count.value = 1; // ✅ 正确,响应式有效,页面会更新const user = ref({ name: "张三" });user.value = { name: "李四" }; // ✅ 正确,直接替换整个对象,响应式依然有效// 2. reactive 直接赋值(❌ 响应式丢失)
let user = reactive({ name: "张三" });user = { name: "李四" }; // ❌ 错误!直接赋值会覆盖原始代理对象,响应式丢失// 此时 user 变成了普通对象,后续修改 user.name,页面不会更新原因解析:reactive 代理的是“原始对象本身”,当你直接给 reactive 包装的变量赋值时,相当于把变量指向了一个新的普通对象,原来的 Proxy 代理关系被切断,自然就失去了响应式能力。而 ref 代理的是“包装对象的 value 属性”,赋值时只是修改了 value 的值(无论是基本类型还是引用类型),Proxy 代理关系依然存在,所以响应式不会丢失。
2. 数组/集合的处理差异
对于数组和 Map、Set 等集合类型,两者的处理方式也有差异,新手容易踩坑:
reactive 处理数组:支持直接修改数组的元素、调用数组方法(push、pop、splice 等),都会触发响应式;但不能直接给数组赋值(和对象赋值一样,会丢失响应式)。
ref 处理数组:需要通过 .value 访问数组,修改元素、调用数组方法时,都要加上 .value,同样支持响应式;且可以直接给 .value 赋值新数组,响应式不会丢失。
ts 体验AI代码助手 代码解读 复制代码 // reactive 处理数组 let list = reactive([1, 2, 3]);list.push(4); // ✅ 正确,响应式有效list[0] = 10; // ✅ 正确,响应式有效list = [4, 5, 6]; // ❌ 错误,响应式丢失// ref 处理数组 const list = ref([1, 2, 3]);list.value.push(4); // ✅ 正确,响应式有效list.value[0] = 10; // ✅ 正确,响应式有效list.value = [4, 5, 6]; // ✅ 正确,响应式有效
选型建议(直接抄作业)
结合上面的区别,给大家一个简单直接的选型方案,不用再纠结:
如果是基本类型数据(number、string 等):直接用 ref;
如果是引用类型数据(对象、数组):
不需要“直接赋值整个对象/数组”:用 reactive,更直观;
需要“直接赋值整个对象/数组”:用 ref,避免响应式丢失;
如果是表单绑定:优先用 ref,避免 reactive 赋值导致的响应式丢失问题。
三、重点避坑:响应式丢失的常见场景及解决方案
聊完 ref 和 reactive 的区别,就必须讲“响应式丢失”——这是开发中最常见的问题,很多人改了数据页面不更新,排查半天都找不到原因,其实都是响应式丢失了。下面结合实际开发场景,讲4个最常见的丢失场景,以及对应的解决方案。
场景1:reactive 直接赋值(最常见)
这是最容易踩的坑,前面已经提过,这里再详细说解决方案:
ts
体验AI代码助手
代码解读
复制代码
// 错误示例(响应式丢失)
let user = reactive({ name: "张三" });// 接口请求后,直接赋值新对象user = await api.getUserInfo(); // ❌ 响应式丢失,后续修改 user 无效果// 解决方案1:不直接赋值,修改属性(推荐)
const user = reactive({ name: "", age: 0 });const res = await api.getUserInfo();// 逐个修改属性,保留 Proxy 代理关系user.name = res.name;user.age = res.age; // ✅ 响应式有效// 解决方案2:用 ref 包装(适合需要整体替换的场景)
const user = ref({ name: "张三" });user.value = await api.getUserInfo(); // ✅ 响应式有效,直接替换整个对象场景2:解构 reactive 对象(高频坑)
当你解构 reactive 包装的对象时,解构出来的属性会变成“普通值”,失去响应式能力——因为解构本质是“取值”,取出的是属性的原始值,不再受 Proxy 监控。
ts
体验AI代码助手
代码解读
复制代码
// 错误示例(响应式丢失)
const user = reactive({ name: "张三", age: 20 });// 解构出 name 和 age
const { name, age } = user;// 修改解构后的变量,页面不会更新name = "李四"; // ❌ 响应式丢失(name 是普通字符串,不是响应式的)age = 21; // ❌ 响应式丢失// 解决方案1:不解构,直接访问属性(推荐)user.name = "李四";user.age = 21; // ✅ 响应式有效// 解决方案2:用 toRefs 解构(保留响应式)
import { toRefs } from "vue";const user = reactive({ name: "张三", age: 20 });const { name, age } = toRefs(user); // 用 toRefs 包装后,解构的是 ref 对象name.value = "李四"; // ✅ 响应式有效,需要 .valueage.value = 21; // ✅ 响应式有效补充:toRefs 的作用是“将 reactive 对象的每个属性,都转换成 ref 对象”,这样解构后,每个属性依然是响应式的,修改时需要 .value(和 ref 用法一致)。
场景3:将 reactive 对象的属性赋值给普通变量
和解构类似,把 reactive 对象的某个属性赋值给普通变量,这个普通变量会失去响应式,本质也是“取出了原始值”。
ts
体验AI代码助手
代码解读
复制代码
// 错误示例(响应式丢失)
const user = reactive({ name: "张三" });// 将 user.name 赋值给普通变量 name
let name = user.name;name = "李四"; // ❌ 响应式丢失,页面不会更新// 解决方案1:直接操作 reactive 对象的属性user.name = "李四"; // ✅ 响应式有效// 解决方案2:用 toRef 单独包装单个属性
import { toRef } from "vue";const user = reactive({ name: "张三" });const name = toRef(user, "name"); // 单独包装 name 属性name.value = "李四"; // ✅ 响应式有效注意:toRef 和 toRefs 的区别——toRef 用于“单独包装一个属性”,toRefs 用于“包装所有属性”,按需使用即可。
场景4:数组/集合的不当操作
除了前面说的“reactive 数组直接赋值”,还有两种不当操作会导致响应式丢失:
ts
体验AI代码助手
代码解读
复制代码// 错误示例1:用索引直接替换整个数组元素(针对引用类型元素)const list = reactive([{ name: "张三" }, { name: "李四" }]);// 直接用普通对象替换数组中的元素,会丢失该元素的响应式list[0] = { name: "王五" }; // ❌ 替换后的元素是普通对象,不是响应式的// 解决方案1:修改元素的属性,不替换整个元素list[0].name = "王五"; // ✅ 响应式有效// 解决方案2:用 splice 替换元素(保留响应式)list.splice(0, 1, { name: "王五" }); // ✅ 用 splice 替换,响应式有效// 错误示例2:直接修改数组的 lengthconst list = reactive([1, 2, 3]);list.length = 0; // ❌ 直接修改 length,会导致响应式丢失,后续 push 无效果// 解决方案:用 splice 清空数组list.splice(0); // ✅ 响应式有效,清空数组后,后续 push 正常触发响应四、高阶用法:shallowRef / shallowReactive 用法及场景
聊完 ref 和 reactive,以及响应式丢失,接下来讲带“shallow”前缀的两个 API:shallowRef 和 shallowReactive。很多人觉得这两个 API 没用,其实在“性能优化”场景中,它们能发挥很大作用——核心是“浅响应式”,只监控“表层数据”,不监控“深层数据”。
先明确:浅响应式 vs 深响应式(核心区别)
我们平时用的 ref 和 reactive,都是“深响应式”:
深响应式:无论数据嵌套多少层,修改任何一层的属性,都会触发响应式(比如修改 user.address.city,页面会更新);
浅响应式:只监控“表层数据”,深层数据的修改不会触发响应式(比如修改 user.address.city,页面不会更新;但修改 user.address 本身,页面会更新)。
shallowRef 和 shallowReactive,就是 Vue3 提供的“浅响应式”工具,用于优化性能——当你明确知道“只需要监控表层数据”时,用它们可以减少 Proxy 的代理开销,提升页面性能。
1. shallowRef 用法及场景
shallowRef 是 ref 的“浅响应式版本”,核心特点: https://infogram.com/pdf-1h984wvw0w78z2p https://infogram.com/pdf-1h984wvw0wrlz2p https://infogram.com/pdf-1h1749w0x0g1l2z https://infogram.com/pdf-1h0r6rzo3oypl4e https://infogram.com/pdf-1h0r6rzo3oyow4e https://infogram.com/pdf-1hnq41oy9y8gk23 https://infogram.com/pdf-1h9j6q7oyokxv4g https://infogram.com/pdf-1hnp27e050ypy4g https://infogram.com/pdf-1hnp27e050ywn4g https://infogram.com/pdf-1h984wvw0wkod2p https://infogram.com/pdf-1hxj48m070kd52v https://infogram.com/pdf-1h0r6rzo3on5w4e https://infogram.com/pdf-1hnq41oy9y7ok23 https://infogram.com/pdf-1h984wvw0w9qd2p https://infogram.com/pdf-1h0n25o0m059l4p https://infogram.com/pdf-1hnp27e050wgy4g https://infogram.com/pdf-1h984wvw0w9dz2p https://infogram.com/pdf-1h1749w0x0p5l2z https://infogram.com/pdf-1h9j6q7oyqvv54g https://infogram.com/pdf-1h984wvw0g3zd2p https://infogram.com/pdf-1h984wvw0g3kz2p https://infogram.com/pdf-1hmr6g8mw3d5z2n https://infogram.com/pdf-1h0r6rzo3qprl4e https://infogram.com/pdf-1hnq41oy9j0ep23 https://infogram.com/pdf-1hxj48m07jpl52v https://infogram.com/pdf-1hmr6g8mw37qz2n https://infogram.com/pdf-1h0n25o0mjoyz4p https://infogram.com/pdf-1hxj48m07jm1q2v https://infogram.com/pdf-1h9j6q7oyq7rv4g https://infogram.com/pdf-1h0n25o0mjkoz4p https://infogram.com/pdf-1hnp27e05jk7y4g https://infogram.com/pdf-1h7v4pdv9q9p84k https://infogram.com/pdf-1hnp27e05j5xn4g https://infogram.com/pdf-1h9j6q7oyqy9v4g https://infogram.com/pdf-1hnp27e05j5ly4g https://infogram.com/pdf-1hmr6g8mw317z2n https://infogram.com/pdf-1h9j6q7oyqpp54g https://infogram.com/pdf-1hnp27e05jo1n4g https://infogram.com/pdf-1h984wvw0gqld2p https://infogram.com/pdf-1h0n25o0mj3vl4p https://infogram.com/pdf-1hnq41oy9jnlk23 https://infogram.com/pdf-1hxj48m07jvwq2v https://infogram.com/pdf-1h0n25o0mjzql4p https://infogram.com/pdf-1h7v4pdv9qj1j4k https://infogram.com/pdf-1hmr6g8mw3kez2n https://infogram.com/pdf-1hnq41oy9jq1k23 https://infogram.com/pdf-1hxj48m07j1qq2v https://infogram.com/pdf-1hmr6g8mw3kro2n https://infogram.com/pdf-1h0n25o0mjdll4p https://infogram.com/pdf-1hmr6g8mw3ywz2n https://infogram.com/pdf-1h1749w0xj7nl2z https://infogram.com/pdf-1h1749w0xj8dl2z https://infogram.com/pdf-1h7v4pdv9qkr84k https://infogram.com/pdf-1hnq41oy9j1yk23 https://infogram.com/pdf-1hxj48m07j8zq2v https://infogram.com/pdf-1hmr6g8mw39vo2n https://infogram.com/pdf-1h7v4pdv9qrnj4k https://infogram.com/pdf-1h0r6rzo3qgvw4e https://infogram.com/pdf-1h9j6q7oyqdd54g https://infogram.com/pdf-1hxj48m07jwdq2v https://infogram.com/pdf-1h7v4pdv9q0wj4k https://infogram.com/pdf-1h984wvw0g1zz2p https://infogram.com/pdf-1h1749w0xjq8l2z https://infogram.com/pdf-1h9j6q7oyq5554g https://infogram.com/pdf-1hxj48m07jqgq2v https://infogram.com/pdf-1hmr6g8mw3j5o2n https://infogram.com/pdf-1hnq41oy9jy5p23 https://infogram.com/pdf-1hnp27e05j05y4g https://infogram.com/pdf-1h0r6rzo3qo8w4e https://infogram.com/pdf-1h984wvw0gwkd2p https://infogram.com/pdf-1hxj48m07jj152v https://infogram.com/pdf-1h1749w0xjjql2z https://infogram.com/pdf-1h9j6q7oyqqq54g https://infogram.com/pdf-1hxj48m07jjlq2v https://infogram.com/pdf-1hmr6g8mw33po2n https://infogram.com/pdf-1h9j6q7oyqr0v4g https://infogram.com/pdf-1hnp27e05jgny4g https://infogram.com/pdf-1hmr6g8mw3ngz2n https://infogram.com/pdf-1h0n25o0mjrjz4p https://infogram.com/pdf-1h7v4pdv9q5y84k https://infogram.com/pdf-1h9j6q7oyqee54g https://infogram.com/pdf-1hnq41oy9jerk23 https://infogram.com/pdf-1h7v4pdv9qljj4k https://infogram.com/pdf-1hxj48m07jg852v https://infogram.com/pdf-1h9j6q7oyq9954g https://infogram.com/pdf-1hnp27e05jd9n4g https://infogram.com/pdf-1h0n25o0mjlml4p https://infogram.com/pdf-1h9j6q7oyqkjv4g https://infogram.com/pdf-1hnq41oy9jzxp23 https://infogram.com/pdf-1h0r6rzo3q5ow4e https://infogram.com/pdf-1hnq41oy9jzgk23 https://infogram.com/pdf-1hxj48m07jexq2v https://infogram.com/pdf-1h1749w0xj9xq2z https://infogram.com/pdf-1hnq41oy9jrlp23 https://infogram.com/pdf-1h0r6rzo3q9qw4e https://infogram.com/pdf-1h9j6q7oyqll54g https://infogram.com/pdf-1hmr6g8mw30wo2n https://infogram.com/pdf-1h984wvw0gmgz2p https://infogram.com/pdf-1h0n25o0mjn1z4p https://infogram.com/pdf-1h1749w0xje3q2z
只监控 .value 的“表层变化”,不监控 .value 内部的深层变化;
用法和 ref 一致,修改时需要 .value;
适合:包装“深层嵌套较少”或“只需要整体替换”的引用类型数据。
ts
体验AI代码助手
代码解读
复制代码
import { shallowRef } from "vue";// 用 shallowRef 包装引用类型数据
const user = shallowRef({ name: "张三", address: { city: "北京" } });// 场景1:修改 .value 本身(表层变化,✅ 触发响应式)user.value = { name: "李四", address: { city: "上海" } }; // ✅ 页面会更新// 场景2:修改 .value 内部的深层属性(深层变化,❌ 不触发响应式)user.value.name = "李四"; // ❌ 页面不更新user.value.address.city = "上海"; // ❌ 页面不更新// 补充:如果想让深层变化触发响应式,可以手动调用 triggerRef
import { shallowRef, triggerRef } from "vue";user.value.address.city = "上海";triggerRef(user); // ✅ 手动触发响应式,页面会更新适用场景:比如“弹窗的显示/隐藏”(只需要修改 visible.value = true/false)、“表格数据的整体刷新”(只需要替换整个表格数据),这些场景用 shallowRef,性能比 ref 更好。
2. shallowReactive 用法及场景
shallowReactive 是 reactive 的“浅响应式版本”,核心特点:
只监控“表层属性”的变化,不监控深层属性的变化;
用法和 reactive 一致,无需 .value;
适合:包装“结构固定、深层属性无需响应式”的引用类型数据。
ts
体验AI代码助手
代码解读
复制代码
import { shallowReactive } from "vue";// 用 shallowReactive 包装对象const user = shallowReactive({ name: "张三", address: { city: "北京", area: "朝阳" }
});// 场景1:修改表层属性(✅ 触发响应式)user.name = "李四"; // ✅ 页面会更新user.address = { city: "上海", area: "浦东" }; // ✅ 页面会更新(替换表层属性)// 场景2:修改深层属性(❌ 不触发响应式)user.address.city = "上海"; // ❌ 页面不更新user.address.area = "浦东"; // ❌ 页面不更新// 补充:无法像 shallowRef 那样手动触发,只能通过修改表层属性触发适用场景:比如“页面配置项”(配置项结构固定,只需要修改表层配置,深层配置无需响应式)、“静态数据的轻微修改”,用 shallowReactive 可以减少代理开销。
shallow* 注意事项(避坑)
不要用 shallowRef 包装“需要频繁修改深层属性”的数据,否则会频繁手动调用 triggerRef,反而增加工作量;
shallowReactive 依然不能直接赋值(和 reactive 一样),赋值会导致响应式丢失;
浅响应式的核心是“性能优化”,如果不确定是否需要,优先用 ref 和 reactive(深响应式),避免因浅响应式导致的“数据修改不更新”问题。
五、总结 + 下一篇悬念
今天咱们从“基础区别”到“深层差异”,再到“响应式丢失避坑”和“shallow* 用法”,由浅入深讲透了 ref、reactive 相关的核心知识点,总结一下重点:
ref 适用于基本类型,也可用于引用类型(需 .value);reactive 只适用于引用类型(无需 .value);
reactive 直接赋值、解构、属性赋值给普通变量,都会导致响应式丢失,用 ref 或 toRefs/toRef 可解决;
shallowRef/shallowReactive 是浅响应式,用于性能优化,只监控表层数据,深层修改需手动触发(仅 shallowRef 支持)。
看到这里,你已经比很多中级程序员更懂 Vue3 响应式了!但还有一个高频问题没讲:
比如,ref 和 reactive 可以互相转换吗?toRefs 和 toRef 的区别到底是什么?还有 readonly、shallowReadonly 这些 API,该怎么用?
这些问题,咱们下一篇接着聊!关注我,下次带你拆解“响应式 API 全家桶”,手把手教你灵活运用所有响应式 API,彻底摆脱“响应式踩坑”的烦恼,面试时也能对答如流~