ref 与 reactive:两种响应式容器
响应式基础:ref、reactive 与 computed · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一节解决两个问题:什么时候用 ref、什么时候用 reactive,以及为什么「解构响应式对象」会让界面失去响应。学完你能为一份数据结构选出正确的容器,并避开响应式丢失的三个高频陷阱。
问题场景
你从接口拿到一个用户对象,用 reactive() 包起来,然后写 const { name, age } = user 方便使用。改 age 时界面纹丝不动——因为解构出来的是普通值,不是响应式的。另一个同事把整个对象 reactive 化后传给子组件,子组件解构 props 后又踩了同一个坑。这两个 bug 长得不一样,根因是同一个:没理解响应式追踪依附在「对象属性访问」上。
核心解释
Vue 3 用 Proxy 实现响应式:把一个普通对象包成 Proxy,之后对这个对象的属性读写都会被拦截记录。所以响应式的最小单位是「对象的属性」,不是变量本身。
ref 把任意值包成一个带 .value 的对象,通过 getter/setter 追踪;reactive 返回原对象的 Proxy,直接访问属性即可。两者的分工:
- ref:适用于基本类型(数字、字符串、布尔)和「整体替换」的场景(
user.value = newUser)。模板里自动解包,脚本里要.value。 - reactive:适用于字段多、频繁改属性的对象(表单、配置)。不能整体替换(会丢代理),不能用于基本类型。
<script setup>
import { reactive, ref } from "vue";
const count = ref(0); // 基本类型 → ref
const form = reactive({ // 多字段对象 → reactive
email: "",
password: "",
remember: true,
});
function reset() {
count.value = 0; // ref 用 .value
form.email = ""; // reactive 直接改属性
form.password = "";
}
</script>
<template>
<p>计数:{{ count }}</p>
<input v-model="form.email" placeholder="邮箱" />
<button @click="reset">重置</button>
</template>
三个必须记住的陷阱:
陷阱一:解构丢响应式。 const { email } = form 拿到的是字符串快照,之后 form.email 怎么变它都不会更新。需要解构时用 toRefs:
import { reactive, toRefs } from "vue";
const form = reactive({ email: "", password: "" });
const { email, password } = toRefs(form); // 每一项都是 ref,保持连接
email.value = "a@b.com"; // 等价于 form.email = "a@b.com"
陷阱二:reactive 不能整体替换。 form = reactive({ ... }) 或 form = newForm 会让模板继续指向旧代理。要么逐字段赋值(Object.assign(form, newForm)),要么一开始就用 ref({...}),替换时 form.value = newForm。
陷阱三:ref 在数组/对象里不会自动解包。 const list = ref([ref(1)]),访问 list.value[0] 得到的是 Ref 对象,需要 .value。模板里嵌套在数组中的 ref 也不解包。所以集合里直接放普通值,别套 ref。
示例拆解
一个购物车小计,演示 ref 与 reactive 协作:
<script setup>
import { computed, reactive, ref } from "vue";
const items = reactive([
{ id: 1, name: "键盘", price: 399, qty: 1 },
{ id: 2, name: "鼠标", price: 129, qty: 2 },
]);
const coupon = ref(0); // 优惠金额
const total = computed(() =>
items.reduce((sum, it) => sum + it.price * it.qty, 0) - coupon.value
);
function addQty(id) {
const it = items.find((x) => x.id === id);
if (it) it.qty += 1; // 直接改属性,触发更新
}
</script>
<template>
<ul>
<li v-for="it in items" :key="it.id">
{{ it.name }} × {{ it.qty }} = {{ it.price * it.qty }} 元
<button @click="addQty(it.id)">+1</button>
</li>
</ul>
<p>优惠:<input v-model.number="coupon" type="number" style="width: 80px" /> 元</p>
<p>合计:{{ total }} 元</p>
</template>
注意 items 用 reactive 包数组:数组本身是对象,push、索引赋值都能追踪。coupon 是基本类型所以用 ref,模板里自动解包,v-model.number 会把输入转成数字。
动手任务
- 把上面的
items改成ref([]),同步修改所有访问点(.value),跑一遍确认行为一致。记录两种写法各改了几处。 - 写一个
const profile = reactive({ name: "阿吉", tags: ["vue", "ts"] }),用toRefs解构出name并绑定到输入框,验证修改输入框会同步profile.name。 - 故意制造「解构丢响应式」:
const { name } = profile后改profile.name,观察界面不更新,再改成toRefs版本对比。
常见错误
- 解构 reactive 对象:
const { email } = form后改form.email界面不更新。用toRefs或直接访问form.email。 - 整体替换 reactive 对象:
form = reactive(newData)后模板还连着旧代理。用Object.assign或改用ref。 - 在模板里给 ref 写
.value:模板已自动解包,多写一层取不到值(数组/对象内部的 ref 除外)。 - 以为 reactive 数组的索引赋值不响应:Vue 3 的 Proxy 能追踪索引赋值,
arr[0] = x会更新;但arr.length = 0这类操作要小心,推荐用splice。
检查与复盘
- 能说出 ref 与 reactive 各自适用场景,并各举一个反例
- 能解释「解构丢响应式」的机制,并写出
toRefs修复 - 能判断一段代码里的响应式丢失问题出在哪一行
参考资料
- Vue 官方文档「响应式基础」:ref/reactive 的官方分工与
toRefs - Vue 官方文档「深入响应式系统」:Proxy 追踪机制
读完这一课了?
标记完成后,工作台会安排接下来的学习。