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零依赖
  • axiosCancelToken 已被废弃,官方推荐用 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() 干净利落。

相关推荐
用户02071992077212 小时前
Kafka 与事务 Outbox
前端
IT_陈寒12 小时前
Vite的静态资源路径搞得我头秃,原来这样配才对
前端·人工智能·后端
BerryS3N12 小时前
前端应用的离线暂停更新策略:构建稳定可靠的渐进式部署方案
前端
LaughingZhu12 小时前
Product Hunt 每日热榜 | 2026-07-17
前端·数据库·人工智能·经验分享·mysql·chatgpt·html
潇凝子潇13 小时前
UnsupportedOperationException
java·前端·数据库
卷无止境13 小时前
Python 跨平台 GUI 解决方案全景报告
前端·python
万少19 小时前
用腻了 WorkBuddy 的默认界面?这个开源小工具给它换上了毛玻璃
前端·javascript·后端
海上彼尚1 天前
Nodejs也能写Agent - 16.LangGraph篇 - 条件分支与循环
前端·后端·langchain·node.js
To_OC1 天前
从 “卡死半天” 到 “打字机效果”:大模型流式输出前端实现全记录
前端·javascript·llm
SoaringHeart1 天前
Flutter最佳实践:键盘辅助视图输入框天添加图片附件
前端·flutter