用户先输入 “re”,随后输入 “react”。第二个请求可能更早完成,而第一个请求稍后返回,把正确结果覆盖。这个问题与请求快慢有关,不能靠“平时测试没遇到”来避免。
取消不再需要的请求
AbortController 为 fetch 提供一个取消信号。新的搜索开始时,先取消旧请求。
let controller;
let requestId = 0;
async function search(query) {
controller?.abort();
controller = new AbortController();
const currentId = ++requestId;
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (currentId !== requestId) return;
// 用当前请求的结果更新页面。
renderResults(data);
} catch (error) {
if (error instanceof DOMException &&
error.name === 'AbortError') return;
if (currentId !== requestId) return;
showSearchError('搜索失败,请重试');
}
}这是应用结构示例:/api/search、renderResults 和 showSearchError 需要由实际项目实现。本站文章搜索使用本地索引,不依赖这个接口。
为什么还检查请求编号
取消可以减少无用的工作;请求编号则约束“只有最新请求能更新页面”。当流程包含无法被 signal 取消的处理步骤时,这道检查尤其有用。
fetch 遇到 HTTP 404 或 500 通常不会自动 reject,因此还要检查 response.ok。取消请求是预期行为,不应当显示成用户需要处理的错误。
防抖解决另一件事
防抖减少请求发起次数,取消和编号处理过期结果。它们可以配合,但不能互相替代。即使每 300 毫秒才发一次请求,响应仍可能乱序。
清理也是逻辑的一部分
在组件销毁、页面切换或用户清空搜索词时,取消请求并使旧编号失效。若界面还有 loading 状态,也需要只让当前请求更新它,避免旧请求的 finally 提前关闭新的加载提示。
读到这里,不妨动手试一试。
把一个概念变成能运行的小实验。