AbortController 搜索框实战:取消旧请求,也防止旧结果覆盖新结果
用户连续输入两个关键词,第二次搜索先返回,第一次稍后才完成,如果页面按完成顺序渲染,就可能显示旧关键词的结果。取消旧请求可以减少无用工作,但还需要限制谁有权更新界面。本文依据 DOM 与 Fetch 标准,面向支持 AbortController 的现代浏览器,只演示同源只读搜索,不讨论创建或修改数据的接口。
AI生成概念配图:旧搜索路径被取消,最新搜索结果获得显示权。仅辅助理解,不代表真实界面或实测结果。
把一次请求与一个控制器绑定
AbortController 提供 signal,支持取消的 API 通过它观察终止状态。被取消的 signal 不能重置为未取消,因此每次新搜索应创建新控制器。浏览器取消 fetch 并不证明服务器已经停止计算,也不会撤销已经发生的服务端效果;这里选择只读搜索,正是为了把边界保持清楚。
下面假定页面已经有 query 输入框、status 状态节点和 results 结果节点。代码同时维护递增序号:控制器负责尽量停止旧工作,序号负责防止旧工作修改当前页面。返回数据只作为文本展示,真实项目可以换成经过安全处理的结果渲染组件。
const input = document.querySelector('#query');
const status = document.querySelector('#status');
const results = document.querySelector('#results');
let current = null;
let sequence = 0;
async function search(query) {
const id = ++sequence;
current?.abort();
current = null;
results.textContent = '';
if (!query.trim()) {
status.textContent = '';
return;
}
const controller = new AbortController();
current = controller;
status.textContent = '查询中';
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 (id !== sequence || controller.signal.aborted) return;
results.textContent = JSON.stringify(data, null, 2);
status.textContent = '完成';
} catch (error) {
if (id !== sequence || controller.signal.aborted) return;
status.textContent = '请求失败';
} finally {
if (id === sequence) current = null;
}
}
function onInput() { void search(input.value); }
input.addEventListener('input', onInput);检查状态更新,不只检查数据渲染
请求序号要在发起新工作前推进,并在解析正文之后再次检查。原因是请求可能已拿到响应,后面的异步处理仍在继续;即使某个步骤不响应取消,旧结果也不能夺回显示权。错误提示和 finally 中的清理同样需要检查归属,否则旧请求结束时可能把新请求的加载状态清掉。
Fetch 对 HTTP 失败状态通常仍会返回 Response,所以示例显式检查 response.ok。网络异常、JSON 解析错误和主动取消也不是同一种情况。这里对用户只给出简短失败提示,工程日志应按需要分类,但不要把搜索词、完整响应或敏感地址不加筛选地记录到外部监控。
取消是正常交互,不必显示成故障
用户换了关键词导致旧请求取消,通常不需要弹出红色错误提示。示例依据本次 signal 的状态以及请求序号忽略过时分支,也避免只按异常名称猜测取消来源。若以后引入自定义 abort reason 或其他支持 signal 的操作,应继续保持同一套归属规则,而不是为每个异步步骤单独修改全局状态。
用户清空输入时也应让旧结果失效,而不只是清空屏幕。示例先增加序号并取消旧请求,再处理空输入,防止稍后返回的数据重新填满空列表。防抖可以进一步减少输入过程中的请求次数,但它解决的是发起频率,不能替代取消和结果归属检查。
离开页面时结束这轮工作
function disposeSearch() {
input.removeEventListener('input', onInput);
sequence += 1;
current?.abort();
current = null;
}在组件卸载或不再使用搜索区域时调用清理。示例使用具名事件回调,让 removeEventListener 能移除同一个监听器,再让当前请求失效。如果页面会反复挂载搜索组件,应由组件生命周期统一拥有输入节点、序号和控制器,避免旧实例继续持有新页面的状态。
验收时让旧请求故意慢于新请求,再测试清空输入、解析失败、非成功状态和退出页面。最终页面应只反映当前关键词,且不出现过时错误提示。取消请求只是手段,维护正确的界面状态才是完成标准。


