3.3 computed、watch 与副作用
本章目标: 把“计算一个值”和“发生一件事”分开,避免让多个状态彼此同步。
computed 保存的是规则
src/App.vue
<script setup lang="ts">
import { computed, ref } from 'vue';
const keyword = ref('');
const tasks = ref([
{ id: 1, title: 'JavaScript 基础', isDone: true },
{ id: 2, title: 'Vue 响应式', isDone: false },
]);
const pendingCount = computed(() => tasks.value.filter(t => !t.isDone).length);
const visibleTasks = computed(() => {
const query = keyword.value.trim().toLowerCase();
return tasks.value.filter(t => t.title.toLowerCase().includes(query));
});
</script>
<template>
<main>
<label>搜索 <input v-model="keyword" type="search" /></label>
<p>待完成 {{ pendingCount }} 项</p>
<p v-if="visibleTasks.length === 0">没有匹配的任务</p>
<ul v-else>
<li v-for="task in visibleTasks" :key="task.id">{{ task.title }}</li>
</ul>
</main>
</template>
computed 基于响应式依赖缓存结果,依赖变化后重新计算。不要另设 pendingCount 普通状态再手动维护它,它可以从任务数组直接推导。
computed 应保持纯计算
不要在计算函数中发请求、改写源状态或启动定时器。对数组排序时也不要原地修改源数组,可使用 [...tasks.value].sort(...)。
watch 执行副作用
观察搜索词的脚本片段
import { watch } from 'vue';
watch(keyword, (newValue, oldValue) => {
console.log('搜索词变化', oldValue, '→', newValue);
});
watch 显式指定要观察的来源,适合保存偏好、发请求、调用外部组件等。默认在来源变化时调用,加 { immediate: true } 可以在创建监听器时先调用一次。
观察 reactive 对象中的某个属性时传 getter:
watch(() => form.title, title => {
console.log(title);
});
其中 form 是已声明的 reactive 对象。直接传 form.title 只是字符串值,不能作为该属性的响应式来源。
watchEffect 自动追踪
import { watchEffect } from 'vue';
watchEffect(() => {
console.log(`共有 ${tasks.value.length} 个任务`);
});
watchEffect 立即执行,并追踪同步执行过程中读取的响应式值。异步函数中第一个 await 后才读取的依赖不会自动被收集。
| 场景 | 优先选择 |
|---|---|
| 筛选、计数、格式化 | computed |
| 用户点击保存 | 事件处理函数 |
| 指定状态改变后请求数据 | watch |
| 简单副作用,依赖可自动读取 | watchEffect |
清理过期的异步操作
搜索词快速变化时,旧请求可能比新请求更晚返回。取消过期请求,或者用序号检查结果归属。下面是 watch 的清理写法:
需要服务端提供 /api/search 的机制示例
const results = ref<unknown>(null);
const error = ref('');
watch(keyword, async (query, _previous, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
error.value = '';
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data: unknown = await response.json();
if (!controller.signal.aborted) results.value = data;
} catch (cause) {
if (!controller.signal.aborted) {
error.value = cause instanceof Error ? cause.message : '搜索失败';
}
}
});
清理回调应在发起操作前注册。Vue 3.5+ 另有 onWatcherCleanup,必须在监听回调的同步阶段调用。
练习
增加“已完成数量”和“完成率”。空列表时完成率显示 0%,不能得到 NaN。
参考答案
const doneCount = computed(() => tasks.value.filter(t => t.isDone).length);
const progress = computed(() => tasks.value.length === 0
? 0
: Math.round(doneCount.value / tasks.value.length * 100));
这些都是可推导值,不需要 watch 同步另一组状态。