跳到主要内容
← 返回文章列表
面试题-基础2026/08/07约 2 分钟读完

面试题:搜索请求竞态

搜索请求竞态相关面试题

面试题:搜索请求竞态
文章目录 4

面试题:搜索请求竞态

用户在搜索框中先后输入:

a → ab → abc

前端依次发出三个请求,但由于网络速度不同,响应顺序可能是:

abc → ab → a

请回答:

  1. 这会造成什么问题?

  2. 如何保证页面最终只展示 abc 对应的结果?

  3. 防抖能否彻底解决这个问题?为什么?

请按真实面试的方式作答,控制在 5 分钟内。

我的回答

  1. 这会造成搜索a的结果最终返回覆盖abc的返回

  2. 如果没有其他要求,可以在输入框失焦搜索或者回车或者点击搜索按钮,才返回结果;如果要实时的话,采用防抖能解决,但也未必,它并一定短时间内按出abc,没想到好的解决办法

  3. 防抖不能彻底解决这个问题,假如网速很慢,而它又短时间内未连续暗处bc,可能结果还是a的

题解

整体判断是正确的,尤其准确指出了:防抖只能减少请求数量,不能保证响应顺序。评分:7/10。

正确点:

  • 正确识别了竞态问题:旧请求后返回,覆盖新请求结果。

  • 提出了失焦、回车、点击按钮后再搜索,这在非实时搜索场景中可行。

  • 正确认识到防抖无法彻底解决竞态,因为两个请求仍可能同时处于进行中。

需要补充两种常见方案:

  1. 忽略过期响应

给每次请求分配递增序号,只允许最新请求更新页面:

发出 a,请求序号 1
发出 ab,请求序号 2
发出 abc,请求序号 3

即使响应顺序是 3、2、1
也只有序号 3 可以更新页面
  1. 取消旧请求

每次发起新请求前,通过 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 解决资源问题,尽可能取消无效请求;防抖解决频率问题,减少请求数量。三者职责不同,可以组合使用。

评论

0 条

评论会在审核后展示,邮箱不会公开。

正在确认登录状态

5000 / 5000