AbortController 搜索框实战:取消旧请求,也防止旧结果覆盖新结果

10-01 3阅读

用户连续输入两个关键词,第二次搜索先返回,第一次稍后才完成,如果页面按完成顺序渲染,就可能显示旧关键词的结果。取消旧请求可以减少无用工作,但还需要限制谁有权更新界面。本文依据 DOM 与 Fetch 标准,面向支持 AbortController 的现代浏览器,只演示同源只读搜索,不讨论创建或修改数据的接口。

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 能移除同一个监听器,再让当前请求失效。如果页面会反复挂载搜索组件,应由组件生命周期统一拥有输入节点、序号和控制器,避免旧实例继续持有新页面的状态。

验收时让旧请求故意慢于新请求,再测试清空输入、解析失败、非成功状态和退出页面。最终页面应只反映当前关键词,且不出现过时错误提示。取消请求只是手段,维护正确的界面状态才是完成标准。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。