computed、watch 与清理:别让旧请求覆盖新结果
你先搜索 Vue,马上又搜索 React。React 的结果先回来,Vue 的结果晚一点回来,页面却变成了 Vue。这不是搜索算法错了,而是两个请求完成顺序与输入顺序不同。
先分清计算和副作用
从文章数组计算数量,使用 computed;关键词变化后请求网络数据,使用 watch 或合适的数据获取方案。computed 的 getter 应保持纯计算,别在里面发送请求或修改其他状态。否则“读取一个值”就偷偷触发行为,排错会很费劲。
watch 明确指定观察来源;watchEffect 在同步执行期间收集依赖,异步函数第一次 await 之后才读到的依赖不会自动被这一轮收集。需要精确控制搜索条件时,watch 往往更直观。
一个带清理的搜索片段
以下代码放在 Vue 3 组件的 setup 中。/api/articles?q=... 是教学接口,约定返回标题字符串数组;实际接口需要按契约调整解析。
import { ref, watch } from 'vue'
const query = ref('')
const results = ref<string[]>([])
const loading = ref(false)
const error = ref('')
watch(query, async (value, _previous, onCleanup) => {
const controller = new AbortController()
let active = true
onCleanup(() => {
active = false
controller.abort()
})
results.value = []
error.value = ''
loading.value = false
if (!value.trim()) return
loading.value = true
try {
const response = await fetch(
`/api/articles?q=${encodeURIComponent(value.trim())}`,
{ signal: controller.signal },
)
if (!response.ok) throw new Error('搜索失败,请重试')
const data: unknown = await response.json()
if (!Array.isArray(data) || !data.every((x) => typeof x === 'string'))
throw new Error('搜索结果格式不正确')
if (active) results.value = data
} catch (cause) {
if (active) error.value = cause instanceof Error ? cause.message : '搜索失败'
} finally {
if (active) loading.value = false
}
})
abort 尽量停止无用网络工作,active 防止已经失效的流程提交结果。注意 finally 也要检查,否则旧请求可能把新请求的 loading 提前关掉。取消并不意味着服务端一定没执行,写操作不能把 abort 当成回滚。
清理要和副作用成对
监听事件要移除,计时器要清除,订阅要取消。同步在 setup 中建立的 watcher 会随组件卸载停止;如果在异步回调里才创建 watcher,就需要确认它的归属并主动停止。Vue 3.5 的 onWatcherCleanup 必须在同步阶段注册,本例使用回调参数 onCleanup,同样在 await 前注册,意图更清楚。
更新状态后立刻读取 DOM,可能读到更新前的画面。确实需要操作更新后的 DOM 时考虑 nextTick,或恰当使用 flush: 'post'。不要用一个随意的 100ms 定时器赌更新完成。
练习与参考
让 Vue 请求延迟 800ms、React 请求延迟 100ms,快速切换关键词。最终必须留下 React 的结果,加载指示也不能被旧请求关闭。再测试清空关键词和组件卸载。
给竞态画一条时间线
假设第 0ms 输入 vue,第 100ms 输入 react;React 请求在第 200ms 完成,Vue 请求在第 800ms 才完成。需要保护的是“当前查询拥有结果提交权”,不是“最后收到的响应总是最新”。每轮回调创建自己的 active,清理旧轮时将它设为 false。旧响应即使返回,也不能提交结果或关闭新轮 loading。
watch 默认不会仅因注册就执行回调。本文 query 初始为空,等用户修改后开始搜索;如果要初始有查询就立即加载,可按需求增加 immediate:true。观察对象时还要区分观察整个响应式对象、getter 返回值和深层变化,不能把所有来源都当作简单 ref。
调试时在回调建立、清理、请求返回三个位置记录不含敏感数据的请求序号,故意延迟旧请求,再观察日志。只验证“正常网络下搜一次能出结果”,无法证明竞态处理正确。防抖可以减少请求数量,但旧请求已经发出时,仍然需要失效处理。
若请求改成保存文章,客户端取消只能停止等待或尽量中断传输,不能撤销服务器已提交的事务。保存要使用版本条件、幂等协议或明确的状态查询;不要把读取搜索的清理逻辑当作写入事务方案。
两个清理 API 的限制别混淆
Vue 3.5+ 的 onWatcherCleanup 要在同步执行阶段调用,不能先 await 再注册。回调参数 onCleanup 绑定到 watcher 实例,不受同一条同步调用限制。本章把清理放在 await 前,是为了让副作用一开始就有配套的结束动作,不是说两个 API 的规则完全一样。
练习时先取消旧请求,再观察它的 finally 是否仍会运行。若 finally 无条件写 loading=false,新请求还在等待时指示器就会提前消失。把成功、错误、finally 三条提交路径一起审查,比只检查返回结果更完整。
本节的核验依据
2026-10-25 查阅。下面的链接支持本节的机制说明;练习场景和选型建议是本教程的教学设计。