Fetch 请求竞态终结者:AbortController 不只是用来"取消"的

问题场景

搜索框输入"深圳天气",快速删除再输入"北京天气",结果页面先显示北京、再闪变成深圳------请求竞态污染,老 Bug 了。

用 setTimeout 做防抖能缓解,但用户快速切换时依然复现。核心原因:后发的请求先回来,先发的请求后回来,状态被覆盖。

原因分析

js 复制代码
// 经典错误写法
async function search(keyword) {
  const res = await fetch(`/api/search?q=${keyword}`)
  setResults(await res.json())
}
  • 网络包顺序不可控,请求 B 先返回 → 覆盖请求 A 的结果
  • 组件卸载后继续 setState → 控制台报 "Can't perform a React state update"
  • 防抖只是降低概率,不解决根本问题

解决方案

AbortController 不止能做"取消",还能做请求序列化------每次新请求发出前,自动 abort 上一次未完成的请求。

js 复制代码
// 终极方案:用 AbortController 做"请求串联"
let currentController = null

async function search(keyword) {
  // 1. 中断上一次请求
  if (currentController) {
    currentController.abort()
  }

  // 2. 创建新控制器
  const controller = new AbortController()
  currentController = controller

  try {
    const res = await fetch(`/api/search?q=${keyword}`, {
      signal: controller.signal
    })
    const data = await res.json()

    // 3. 检查是否还是当前请求(避免 abort 后误更新)
    if (controller === currentController) {
      setResults(data)
    }
  } catch (err) {
    // AbortError 是正常行为,不需要报错
    if (err.name === 'AbortError') return
    handleError(err)
  }
}

实操代码:封装成通用 Hook

js 复制代码
// useLatestRequest.js --- 一行接入,杜绝竞态
import { useRef, useCallback } from 'react'

export function useLatestRequest() {
  const controllerRef = useRef(null)

  const run = useCallback(async (url, options = {}) => {
    controllerRef.current?.abort()
    const controller = new AbortController()
    controllerRef.current = controller
    
    const res = await fetch(url, {
      ...options,
      signal: controller.signal
    })
    const data = await res.json()
    
    if (controller !== controllerRef.current) return
    return data
  }, [])

  const cancel = useCallback(() => {
    controllerRef.current?.abort()
  }, [])

  return { run, cancel }
}

使用:

jsx 复制代码
function SearchBox() {
  const { run } = useLatestRequest()
  const [results, setResults] = useState([])

  const handleSearch = async (kw) => {
    const data = await run(`/api/search?q=${kw}`)
    if (data) setResults(data)
  }

  return <input onChange={e => handleSearch(e.target.value)} />
}

要点总结

场景 旧方案 推荐方案
搜索框 防抖 + flag AbortController 串联
分页切换 loading + 忽略旧结果 同上
组件卸载 isUnmounted ref 在 useEffect cleanup 中 abort
详情页跳转 无处理 跳转时 abort 前页请求

值得注意:

  • fetch 原生支持 signal,零依赖
  • axios 的 CancelToken 已被废弃,官方推荐用 AbortController(v0.22+ 支持 signal)
  • aborted 的请求不会收到响应体,节省带宽
  • 配合 AbortSignal.timeout(5000) 可以同时做超时处理
js 复制代码
// 超时 + 竞态双杀
const { run } = useLatestRequest()

// 5秒超时
const data = await run(url, {
  signal: AbortSignal.timeout(5000)
})

下次遇到请求竞态,别再往 useEffect 里塞 flag 了------AbortController 一行 controller.abort() 干净利落。

相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万2 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋2 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端