← 返回题目列表

前端请求竞态是什么?如何避免旧响应覆盖新状态?

中等 第 17 / 26 题 更新于 2026/07/29
前端网络请求竞态AbortController状态管理

简化版

请求竞态是多个请求先后发出,但响应顺序不确定,导致旧请求晚返回后覆盖新请求结果。典型场景是搜索框、分页切换、筛选条件变化和路由切换。解决方式包括取消旧请求、给请求打序号只接收最后一次、按参数 key 写入独立缓存、组件卸载时忽略结果,以及服务端返回版本信息。核心原则是:页面状态只能被当前有效请求更新。

详细版

竞态不是接口错误,而是异步顺序不可控。

场景风险解决
搜索输入旧关键词结果覆盖新关键词取消旧请求或序号判断
分页切换第 1 页晚于第 2 页返回只接收当前页 key
路由切换已离开页面仍 setState卸载清理
自动刷新多批结果交错版本号或时间戳
let latestRequestId = 0

async function search(keyword: string) {
  const id = ++latestRequestId
  const data = await fetch(`/api/search?q=${keyword}`).then(r => r.json())
  if (id === latestRequestId) render(data)
}

竞态处理的关键不是让请求按顺序返回,而是只让有效结果更新界面。

完整版教学

一、请求竞态为什么常见

浏览器发出的请求会经过网络、网关、服务端和缓存,每个请求耗时都不固定。即使 A 比 B 先发出,也可能 B 先返回。

如果代码默认“后发请求一定后返回”,就会产生竞态问题。

二、搜索框案例

用户输入 a 后发请求,马上输入 ab 又发请求。如果 ab 先返回并渲染,随后 a 晚返回又渲染,页面就显示了旧结果。

这种问题在弱网和后端波动时特别明显。

三、方案一:取消旧请求

Fetch 可以使用 AbortController。

let controller: AbortController | null = null

async function load(url: string) {
  controller?.abort()
  controller = new AbortController()
  return fetch(url, { signal: controller.signal })
}

取消可以减少无效网络和服务端压力,但不是所有请求都一定能在服务端停止处理。

四、方案二:只接收最后一次

有时旧请求无法取消,或者取消不是必须。可以用请求序号判断。

let seq = 0
async function loadUser(id: string) {
  const current = ++seq
  const user = await api.getUser(id)
  if (current !== seq) return
  state.user = user
}

这能保证旧响应不会覆盖新状态。

五、方案三:按 key 存储

如果数据本来就属于不同参数,不要共用同一个状态槽。

错误状态更好状态
listlistByQuery[queryKey]
detaildetailById[id]
pageDatapageDataByPage[page]

这样不同请求不会互相覆盖。

六、组件卸载和路由切换

组件卸载后请求返回,如果继续 setState,可能造成警告或状态污染。要在清理函数中取消请求,或用 mounted 标记忽略结果。

React、Vue、Svelte 都有各自的生命周期清理点,原则一致。

七、常见误区与追问

  • 误区:只要 await 就不会竞态。 await 只让当前函数等待,不能保证多个异步任务全局有序。
  • 误区:取消请求后后端一定不处理。 浏览器取消连接不代表服务端业务一定停止,写接口仍需幂等。
  • 误区:所有旧响应都应该丢弃。 如果按 key 缓存,旧响应可以写回对应 key,但不能覆盖当前视图。
  • 追问:AbortError 要怎么处理? 通常识别为主动取消,不作为错误提示给用户。
  • 追问:分页竞态怎么处理? 响应回来时检查当前页码或查询 key 是否一致。
  • 追问:竞态和防抖有什么关系? 防抖减少请求数量,但剩余请求仍可能乱序,需要竞态保护。

八、加强记忆

请求竞态记成“先出发不等于先到站”。解决靠取消旧请求、序号只认最后一次、按 key 存状态和卸载清理,核心是旧响应不能覆盖新界面。