跳到主要内容

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 的 props 解构

Vue 3.5+ 对 <script setup> 中直接解构 defineProps() 提供编译器辅助的响应式处理。这是特定编译场景,不适用于普通 reactive 解构。本手册通过 props.title 写法保持边界清晰。

解包也有边界​

ref 作为 reactive 对象的属性时常会自动解包,但 reactive 数组或 Map 中的 ref 元素不保证同样的自动解包行为。开始阶段避免把 ref 深层混进各种容器,用普通对象模型加一个顶层 ref 即可。

页面更新不是立即完成​

修改状态是同步的,但 Vue 会批量调度 DOM 更新。需要在更新之后读取元素尺寸或聚焦时使用 nextTick:

在已定义的 DOM 引用上操作的片段
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。

参考:响应式基础。