搜索框先输入 cat,很快又改成 car。第一次请求慢,第二次请求快,如果两个响应都能更新页面,晚到的 cat 就可能覆盖 car。
AbortController 可以减少不需要的请求工作,页面还需要明确哪一次操作有资格提交结果。我对照 MDN 的取消机制整理这个例子,用递增版本保护数据、错误和 loading。
取消通知能影响哪些操作
fetch 支持 AbortSignal,请求或响应体读取尚未完成时,可以响应中止。已经完成的 Promise 不会因此重新回到等待状态。自行编写的后续异步转换,如果没有处理这个 signal,也可能继续运行。
所以每次输入变化,除了取消旧 controller,还要使旧版本失效。提交状态前再核对一次,才能避免旧任务把自己的结果写回当前页面。
把检查与状态更新放在同一处
下面是浏览器 TypeScript 示例。页面需要把 ui 状态绑定到实际界面,并提供返回 JSON 的 /api/search 接口。可以在一个独立搜索组件里使用这一组状态。
let version = 0;
let controller: AbortController | undefined;
let disposed = false;
const ui = {
data: null as unknown,
error: null as string | null,
loading: false,
};
async function search(query: string): Promise<void> {
if (disposed) return;
const mine = ++version;
controller?.abort();
const current = new AbortController();
controller = current;
ui.loading = true;
ui.error = null;
const canCommit = () =>
!disposed && mine === version && !current.signal.aborted;
try {
const response = await fetch(
"/api/search?q=" + encodeURIComponent(query),
{ signal: current.signal },
);
if (!response.ok) throw new Error("HTTP " + response.status);
const data: unknown = await response.json();
if (canCommit()) ui.data = data;
} catch (error) {
if (canCommit()) {
ui.error = error instanceof Error ? error.message : String(error);
}
} finally {
if (canCommit()) ui.loading = false;
}
}
function dispose(): void {
disposed = true;
++version;
controller?.abort();
ui.loading = false;
}
每次 search 都生成新的版本和 controller。旧任务即使进入 catch 或 finally,也要先通过 canCommit。组件卸载时调用 dispose,使所有未完成操作失去提交资格。重新挂载组件时,应重新创建这一组状态。
这段代码讨论浏览器 fetch,Python 没有同名的 AbortController 官方接口,异步客户端需要按自身的取消规则实现,不能直接照搬。
loading 也需要保护
只检查成功结果,还会遗漏一个很隐蔽的问题。旧请求被取消后进入 finally,如果它无条件把 loading 设为 false,新请求还在运行,界面就已经显示加载结束。
错误状态同样如此。旧请求晚到的错误,不应该覆盖最新一次搜索的成功结果。把三个状态的提交规则放在同一处,可以避免不同分支采用不同版本条件。
如果拿到结果之后还需要 await 一次额外转换,就把提交前检查放到转换之后。检查完版本,又等待一段异步工作,最后直接更新,仍然可能提交过时的数据。
用乱序完成检查这个行为
验证时,让 cat 的处理停在某段异步转换里,再启动 car。car 完成以后才放行 cat,最后页面应该仍然保留 car。这个用例能覆盖请求已经返回、后续处理却还没结束的情况。
再让旧请求先结束、新请求继续等待,检查 loading 是否仍然为 true。最后测试卸载,所有未完成任务都不应继续提交状态。
防抖可以减少请求次数,版本检查决定结果能否提交。这两项可以一起使用。浏览器取消也不会自动撤销服务端已经执行的写操作,提交任务或创建订单时还要单独处理业务状态与幂等。
我是程序员Sunday。完整机制和更多追问见 sunday面试指南的 AbortController 与请求竞态解析。