深浅主题
异步请求乱序覆盖
问题现象
先查询 A,随后查询 B,但 A 较晚返回并覆盖 B 的结果;loading 提前消失或旧错误提示出现在新查询中。
适用环境
现代浏览器 fetch、Vue 3;适用于筛选查询和自动补全。
本文是可复用的排障示例,不代表某次个人生产事故。命令中的名称、账号和数据均为示例,执行前请替换为已确认的目标。
复现或诊断步骤
- 浏览器 Network 开启限速,给 A 添加可控延迟,再立即触发 B。
- 记录递增请求编号、发起条件和结束时间,不记录令牌。
- 检查成功、catch 和 finally 三个分支是否都验证请求仍是当前请求。
原因分析
请求完成顺序不等于发起顺序。只在成功分支校验编号,会让旧请求的 finally 关闭新请求的 loading;只取消网络请求,也不能覆盖所有已经进入业务处理的异步阶段。
修复示例
ts
import { ref } from 'vue'
const rows = ref<unknown[]>([])
const loading = ref(false)
const error = ref('')
let revision = 0
let controller: AbortController | undefined
async function load(keyword: string) {
const id = ++revision
controller?.abort()
const request = new AbortController()
controller = request
loading.value = true
error.value = ''
try {
const res = await fetch('/api/items?q=' + encodeURIComponent(keyword), { signal: request.signal })
if (!res.ok) throw new Error('HTTP ' + res.status)
const data = await res.json()
if (id === revision) rows.value = data
} catch (e) {
if (id === revision && !request.signal.aborted) error.value = String(e)
} finally {
if (id === revision) loading.value = false
}
}把分页、筛选和排序合成一次请求快照。卸载时递增 revision 并 abort,阻止旧组件提交结果。取消查询请求与取消服务器写操作不同,提交类操作仍需服务端幂等控制。
验证方法
测试 A 慢 B 快、A 失败 B 成功、清空条件、离开页面四种情况;最终列表、错误提示和 loading 必须全部对应最后一次有效查询。
常见误区
不要只把 loading 当作互斥锁,这会丢弃用户后续筛选。AbortError 是预期取消,通常不应弹出网络故障提示。