3.2 ref、reactive 与响应式
本章目标: 知道状态为什么会触发页面更新,选择合适的状态声明方式,并避开解构造成的追踪丢失。
ref:推荐的起点
import { ref } from 'vue';
const count = ref(0);
const tasks = ref([{ id: 1, title: '学习响应式', isDone: false }]);
count.value++;
tasks.value[0]!.isDone = true;
tasks.value = tasks.value.filter(t => !t.isDone);
最后两个操作分别是修改内部对象与替换整个数组,都会被普通 ref 的深层响应式处理追踪。脚本使用 .value,模板中顶层 ref 自动解包。
初学阶段优先用 ref 声明状态。它同时支持原始值、对象和整体替换,规则更一致。对象没有必要因为“字段多”就强行改成 reactive。
reactive:对象代理
import { reactive } from 'vue';
const form = reactive({ title: '', isDone: false });
form.title = '学习 Vue';
Vue 用 Proxy 拦截对象属性的读取和修改,从而追踪依赖。reactive 适用于对象,不用于直接包装 number、string 或 boolean。
| 选择 | 脚本访问 | 整体替换 |
|---|---|---|
ref(0) | count.value | 修改 .value |
ref({ ... }) | state.value.title | 可以替换 .value |
reactive({ ... }) | state.title | 通常保留代理引用,修改内部属性 |
将 reactive 变量重新指向一个新对象,会让依赖旧代理的代码失去关联。如果经常从 API 替换完整对象,用 ref 更直接。
解构陷阱
const state = reactive({ count: 0 });
const { count } = state;
state.count++;
console.log(count); // 仍然是 0
解构得到的是读取时的数字,不是追踪属性的“活引用”。需要解构时使用 toRefs:
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0 });
const { count } = toRefs(state);
count.value++;
console.log(state.count); // 1
如果解构的是嵌套对象,其代理引用可能仍然可追踪,但替换原属性后解构变量不会自动指向新对象。不要用这类例外推断“所有解构都安全”。
Vue 3.5+ 对 <script setup> 中直接解构 defineProps() 提供编译器辅助的响应式处理。这是特定编译场景,不适用于普通 reactive 解构。本手册通过 props.title 写法保持边界清晰。
解包也有边界
ref 作为 reactive 对象的属性时常会自动解包,但 reactive 数组或 Map 中的 ref 元素不保证同样的自动解包行为。开始阶段避免把 ref 深层混进各种容器,用普通对象模型加一个顶层 ref 即可。
页面更新不是立即完成
修改状态是同步的,但 Vue 会批量调度 DOM 更新。需要在更新之后读取元素尺寸或聚焦时使用 nextTick:
import { nextTick, ref } from 'vue';
const visible = ref(false);
const input = ref<HTMLInputElement | null>(null);
async function openEditor() {
visible.value = true;
await nextTick();
input.value?.focus();
}
模板应有 <input v-if="visible" ref="input" />。这不是等待固定毫秒数,而是等待 Vue 当前调度的 DOM 更新。
练习
用 ref 保存任务数组,点击按钮后添加一条任务。再用 filter 删除它,观察整体替换是否正常更新列表。
参考答案
const tasks = ref<{ id: number; title: string }[]>([]);
let nextId = 1;
function addTask() {
tasks.value.push({ id: nextId++, title: '新任务' });
}
function removeTask(id: number) {
tasks.value = tasks.value.filter(t => t.id !== id);
}
这个 ID 只适合单次会话的本地练习,连接 API 后使用服务端生成的 ID。
参考:响应式基础。