
文章目录 4
面试题:搜索请求竞态
用户在搜索框中先后输入:
a → ab → abc前端依次发出三个请求,但由于网络速度不同,响应顺序可能是:
abc → ab → a请回答:
这会造成什么问题?
如何保证页面最终只展示
abc对应的结果?防抖能否彻底解决这个问题?为什么?
请按真实面试的方式作答,控制在 5 分钟内。
我的回答
这会造成搜索a的结果最终返回覆盖abc的返回
如果没有其他要求,可以在输入框失焦搜索或者回车或者点击搜索按钮,才返回结果;如果要实时的话,采用防抖能解决,但也未必,它并一定短时间内按出abc,没想到好的解决办法
防抖不能彻底解决这个问题,假如网速很慢,而它又短时间内未连续暗处bc,可能结果还是a的
题解
整体判断是正确的,尤其准确指出了:防抖只能减少请求数量,不能保证响应顺序。评分:7/10。
正确点:
正确识别了竞态问题:旧请求后返回,覆盖新请求结果。
提出了失焦、回车、点击按钮后再搜索,这在非实时搜索场景中可行。
正确认识到防抖无法彻底解决竞态,因为两个请求仍可能同时处于进行中。
需要补充两种常见方案:
- 忽略过期响应
给每次请求分配递增序号,只允许最新请求更新页面:
发出 a,请求序号 1
发出 ab,请求序号 2
发出 abc,请求序号 3
即使响应顺序是 3、2、1
也只有序号 3 可以更新页面- 取消旧请求
每次发起新请求前,通过 AbortController 取消上一次请求。
工程中通常组合使用:
防抖:减少请求数量
AbortController:减少无效请求和资源消耗
请求序号:防止无法取消或已经返回的旧请求覆盖结果表达上可以改为:
防抖只控制请求的发起频率,不控制请求完成顺序。即使经过防抖,只要前后两个请求都已经发出,就仍然可能发生旧响应覆盖新响应。因此还需要取消旧请求,或者在更新状态前判断响应是否属于最新请求。
追问:
如果让你用一个递增的 requestId 解决问题,你会把它定义在哪里?响应返回时,又应该进行什么判断?可以写伪代码回答。
追问解答
核心方案是:在组件或搜索逻辑的外层维护一个递增的 requestId。每次搜索时生成本次请求 ID;响应回来后,只有本次 ID 等于最新 ID,才能更新页面。
let latestRequestId = 0;
async function search(keyword: string): Promise<void> {
// 每次搜索生成新的请求序号
const currentRequestId = ++latestRequestId;
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(keyword)}`,
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data: unknown = await response.json();
// 请求期间可能已经发起了更新的搜索
if (currentRequestId !== latestRequestId) {
return; // 当前响应已经过期,禁止更新页面
}
renderSearchResult(data);
} catch (error) {
// 过期请求的错误也不应该覆盖最新请求的状态
if (currentRequestId !== latestRequestId) {
return;
}
showSearchError(error);
}
}假设发起顺序是:
a → requestId = 1
ab → requestId = 2
abc → requestId = 3即使响应顺序是:
abc → ab → a处理结果也是:
requestId 3 === latestRequestId 3 → 更新页面
requestId 2 !== latestRequestId 3 → 忽略
requestId 1 !== latestRequestId 3 → 忽略如果还要节省网络和服务端资源,可以配合 AbortController:
let latestRequestId = 0;
let activeController: AbortController | null = null;
async function search(keyword: string): Promise<void> {
const currentRequestId = ++latestRequestId;
// 取消上一次请求
activeController?.abort();
const controller = new AbortController();
activeController = controller;
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(keyword)}`,
{ signal: controller.signal },
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data: unknown = await response.json();
if (currentRequestId !== latestRequestId) {
return;
}
renderSearchResult(data);
} catch (error) {
if (currentRequestId !== latestRequestId) {
return;
}
if (error instanceof DOMException && error.name === 'AbortError') {
return;
}
showSearchError(error);
} finally {
// 防止旧请求清理掉新请求的 controller
if (currentRequestId === latestRequestId) {
activeController = null;
}
}
}面试时可以总结为:
requestId解决正确性问题,保证只有最新响应能更新状态;AbortController解决资源问题,尽可能取消无效请求;防抖解决频率问题,减少请求数量。三者职责不同,可以组合使用。
相关文章
- 面试题-基础
Vue 3 响应式解构:为什么替换 user 后出现“李四 / 王五”
通过一道 Vue 3 代码题,区分 reactive 对象解构、嵌套属性修改与对象替换的行为,并用 toRef 保持源属性关联。文章保留首次作答及追问补答,说明从误判到纠正的过程。
- 面试题-基础
JavaScript 浅拷贝、深拷贝与 structuredClone:前端状态更新的正确方式
通过一道对象复制代码题,梳理 JavaScript 浅拷贝、JSON 深拷贝、structuredClone 和 transferable object 的边界,并解释 React 结构共享与 Vue 3 响应式更新的差异。
- 面试题-基础
Cookie 登录接口中的 CSRF:同源策略、CORS、SameSite 与 CSRF Token
通过修改邮箱的跨站表单攻击,梳理 CSRF 的成立条件,解释为什么“读不到响应”不代表攻击失败,并比较 SameSite、CSRF Token、Origin 校验与限流的职责边界。
评论
0 条评论会在审核后展示,邮箱不会公开。