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

面试题:异步搜索中的请求竞态与状态一致性

这道中级场景题考察异步编程、请求取消,以及真实业务中的前端状态管理。

面试题:异步搜索中的请求竞态与状态一致性
文章目录 9

题目

搜索框每次输入都会发送请求:

let data: unknown[] = [];
let loading = false;
let error: string | null = null;

async function search(keyword: string) {
  loading = true;
  error = null;

  try {
    const response = await fetch(
      `/api/search?q=${encodeURIComponent(keyword)}`
    );

    data = await response.json();
  } catch {
    error = "搜索失败";
  } finally {
    loading = false;
  }
}

用户快速输入 "vue",随后改成 "react"。react 请求先返回,vue 请求后返回,最终页面却展示了 vue 的结果。

请回答:

  1. 这段代码存在哪些竞态问题?

  2. 如何保证只有最新请求可以修改 data、error 和 loading?

  3. 请使用 TypeScript、 和必要的状态保护机制重写它。

  4. 为什么只调用 abort() 仍不一定足以保证状态一致?

  5. 应如何区分用户主动取消产生的异常与真正的网络错误?

我的回答

  1. 存在搜索竞态问题,短时间内发起的多次请求,无法保证内容呈现最新发起请求的内容

  2. 外层赋予一个变量request_id,每次请求赋值其一个唯一的key,保证最新的request_id才执行内容写入;增加AbortController,当发起新的请求,取消旧的请求

let data: unknown[] = [];
let loading = false;
let error: string | null = null;
let abort:AbortController | null = null;

async function search(keyword: string) {
  loading = true;
  error = null;
if(abort){
	abort.abort();
}
const signal = new AbortController();

  try {
    const response = await fetch(
      `/api/search?q=${encodeURIComponent(keyword)}`,
	{signal}
    );

    data = await response.json();
  } catch {
    error = "搜索失败";
  } finally {
    loading = false;
  }
}
  1. 这里的状态一致不太明白

  2. reject会返回一个一个名为 AbortError 的 DOMException,通过catch中捕获来识别是否未主动取消

题解

整体思路正确,尤其是“请求版本号 + AbortController”的组合。不过代码里有两个关键错误,而且目前只考虑了 data,还没有保证 loading 和 error 的一致性。

评分:6.5 / 10。

正确点

  • 正确识别了“后发请求可能先返回”的竞态问题。

  • 知道可以使用唯一 requestId 判断响应是否过期。

  • 知道新请求开始时应取消旧请求。

  • 知道取消请求通常会抛出 name === "AbortError" 的 DOMException。

代码中的问题

首先,创建的 controller 没有保存到外层变量:

const signal = new AbortController();

这里变量实际是 AbortController,并不是 AbortSignal。而且没有执行:

abort = new AbortController();

其次,fetch 需要接收 controller.signal:

fetch(url, {
  signal: abort.signal
});

你描述了 request_id,但代码中尚未实现,因此过期响应仍可能更新页面。

“状态一致性”是什么意思?

不仅 data 必须属于最新请求,loading 和 error 也必须属于最新请求。

例如:

请求 A 开始:loading = true
请求 B 开始:取消请求 A,loading = true
请求 A 进入 catch:error = "搜索失败"
请求 A 进入 finally:loading = false
请求 B 仍在等待响应

此时就产生了错误状态:

  • 请求 B 明明还在进行,页面却显示 loading = false

  • 请求 A 是主动取消的,页面却显示“搜索失败”

因此,请求 A 的 catch 和 finally 也不能随意更新状态。不能只保护下面这一句:

data = result;

还需要保护:

error = ...
loading = false

为什么只有 abort() 还不够?

abort() 是资源优化和尽早终止机制,不应被当作唯一的状态正确性保障。

可能出现:

  • 旧请求已经完成网络阶段,正在执行 response.json()

  • 旧请求已经拿到结果,正在执行后续异步逻辑

  • 旧请求被取消后,它的 catch 和 finally 仍然会执行

  • 某些封装库或异步操作并不真正支持取消

因此通常使用两层保障:

AbortController:尽量停止旧请求,节省资源
requestId:禁止过期任务修改最新状态

AbortError 判断

你的方向正确,可以更严谨地写成:

if (err instanceof DOMException && err.name === "AbortError") {
  // 主动取消,不展示为请求失败
}

另外,fetch 遇到 HTTP 404、500 默认不会进入 catch,还需要检查:

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

完整的代码片段

let data: unknown[] = [];
let loading = false;
let error: string | null = null;

let currentRequestId = 0;
let activeController: AbortController | null = null;

async function search(keyword: string): Promise<void> {
  // 先让旧请求失效,再取消它。
  // 即使取消后旧请求进入 catch/finally,也无法修改最新状态。
  const requestId = ++currentRequestId;

  activeController?.abort();

  const controller = new AbortController();
  activeController = controller;

  loading = true;
  error = null;

  try {
    const response = await fetch(
      `/api/search?q=${encodeURIComponent(keyword)}`,
      {
        signal: controller.signal,
      }
    );

    // fetch 遇到 4xx、5xx 默认不会抛出异常
    if (!response.ok) {
      throw new Error(`请求失败:HTTP ${response.status}`);
    }

    const result: unknown = await response.json();

    if (!Array.isArray(result)) {
      throw new Error("服务端返回的数据格式不正确");
    }

    // 只有最新请求才能写入数据
    if (requestId !== currentRequestId) {
      return;
    }

    data = result;
  } catch (cause: unknown) {
    // 过期请求不能修改最新请求的 error
    if (requestId !== currentRequestId) {
      return;
    }

    // 主动取消不属于业务错误
    if (
      cause instanceof DOMException &&
      cause.name === "AbortError"
    ) {
      return;
    }

    error = cause instanceof Error
      ? cause.message
      : "搜索失败";
  } finally {
    // 过期请求不能结束最新请求的 loading
    if (requestId === currentRequestId) {
      loading = false;

      if (activeController === controller) {
        activeController = null;
      }
    }
  }
}

评论

0 条

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

正在确认登录状态

5000 / 5000