搜索框为什么会显示旧结果:用 AbortController 处理请求竞态
搜索联想里的旧结果从哪里来
在搜索框中连续输入“云”和“云原生”时,前端可能先后发出两次请求。网络并不保证响应按发送顺序返回:第二次查询可能先完成,第一次查询随后才返回。如果每次响应都直接更新页面,较旧的结果就会覆盖用户刚输入的新结果。
这类问题通常叫请求竞态。处理它时,可以把三件事分开:防抖减少请求次数,AbortController 尝试取消已经过时的请求,序号检查保证只有最新输入能更新界面。三者解决的问题不同,组合使用更稳妥。
一套适用于搜索框的实现
下面的代码不依赖框架。renderResults、setLoading、showHint 和 showError 是页面自己的展示函数。
let searchTimer;
let activeController;
let requestVersion = 0;
function scheduleSearch(rawValue) {
const query = rawValue.trim();
const version = ++requestVersion;
clearTimeout(searchTimer);
activeController?.abort();
activeController = null;
setLoading(false);
if (!query) {
renderResults([]);
showHint("请输入关键词");
return;
}
searchTimer = setTimeout(() => runSearch(query, version), 250);
}
async function runSearch(query, version) {
const controller = new AbortController();
activeController = controller;
setLoading(true);
try {
const response = await fetch(
"/api/search?q=" + encodeURIComponent(query),
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
const result = await response.json();
if (version !== requestVersion) return;
renderResults(result.items);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") return;
if (version === requestVersion) {
showError("搜索失败,请稍后重试");
}
} finally {
if (version === requestVersion) {
activeController = null;
setLoading(false);
}
}
}
searchInput.addEventListener("input", event => {
scheduleSearch(event.target.value);
});
为什么需要三道处理
第一,防抖把“每敲一个字就发请求”改成“停顿一小会儿再请求”。示例中的 250 毫秒只是一个可调整的起点,具体值应根据产品体验和接口耗时决定。防抖不能处理已经发出的请求:用户开始下一次输入时,旧请求仍可能在网络中。
第二,每次输入都增加 requestVersion,并立刻取消旧控制器。序号在发请求之前更新,因此即使旧请求恰好已经收到响应、取消来不及阻止它完成,旧序号也不能通过检查。它是避免旧数据覆盖新数据的最后一道保护。
第三,AbortError 是主动取消的正常结果,不应显示成“搜索失败”。其他网络错误仍要处理。Fetch 对 HTTP 404 或 500 通常仍会返回 Response,所以代码还检查 response.ok;否则错误页面可能被误当成有效搜索结果。
接入时容易遗漏的细节
清空输入框也会增加序号并取消请求,再清空结果。这样,之前尚未完成的搜索不会在稍后把结果重新写回来。新输入到真正发出请求之间有防抖等待期,旧结果已经失效,可以选择清空列表或显示“输入中”,不要继续把旧结果标成当前结果。
取消请求能让浏览器放弃等待响应或读取响应体,但它不等于服务器一定停止了已经开始的计算。因此服务端仍需设置合理的超时与负载保护;前端取消主要改善界面正确性和客户端资源使用。
用三个场景验证
- 人为让第一次请求变慢,快速输入第二个词。第二次结果应保留,旧响应不能覆盖它。
- 请求尚未完成时清空输入框。结果列表应保持清空,取消操作不应出现红色错误提示。
- 让接口返回 500。界面应显示可理解的失败状态,并结束加载指示,而不是把响应内容当成结果。
调试时可以在浏览器开发者工具的 Network 面板调整网络速度,并查看旧请求是否被取消、最终界面是否对应最新关键词。不要只检查控制台有没有报错,真正要验证的是乱序响应时页面仍然正确。
参考文档:MDN《AbortController》接口文档;MDN《Using the Fetch API》中的“Canceling a request”和“Checking response status”章节。可在 MDN 站内按标题检索。
- 点赞
- 收藏
- 关注作者
评论(0)