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() 干净利落。

相关推荐
雪隐24 分钟前
个人电脑玩AI-16让5060 Ti给你打工——5060Ti 16G 跑 MiniMax-Music-3:从下载到 60s 出歌的全流程
前端·人工智能·后端
CAD老兵1 小时前
让 AI Coding Agent 直接访问 CAD 文档:GitMCP 实战指南
前端·人工智能·github
程序员鱼皮2 小时前
DeepSeek Harness 最新邪修曝光!被吹成核弹的极简模式,真的夯吗?
前端·后端·ai编程
袅沫2 小时前
Nginx中部署多个前端项目——区分路径
服务器·前端
Asize2 小时前
为什么写代码前要先规划组件树?Next.js + Redis 笔记系统实战
前端·javascript·next.js
用户921080262862 小时前
6. 数据大屏 WebSocket 稳定性优化:心跳检测、断点续传和消息去重
前端
柚yuzumi2 小时前
CSS 定位布局:让元素各就各位
前端·css
光影少年2 小时前
react navite图片加载优化、大图卡顿、缓存策略
前端·react native·react.js
小林ixn2 小时前
用 Next.js 和 Redis 撸一个 Markdown 笔记系统:RSC 实战与组件化拆解
前端·redis·next.js
渣波2 小时前
重构旅行体验:基于 React 的 AI 旅游助手对话系统实战解析
前端·javascript