当前位置: 首页 > news >正文

一文搞懂前端请求超时与取消:从 Promise.race 到 AbortController

你可能一直在“假超时”

线上常见需求就两个:

  1. 超过 N 秒没返回,就当失败(超时)
  2. 用户点了下一页 / 切换 Tab / 组件卸载时,别再让旧请求回来覆盖新数据(取消)

很多人第一反应是用Promise.race包一层超时 Promise。它能解决“超时后走 catch”,但不一定能取消浏览器里已经发出去的 HTTP 请求——资源仍可能被占用,竞态问题也可能还在。

本文按「能跑 → 能取消 → 能避坑」写,示例以浏览器原生fetch+AbortController为主(现代前端默认栈),并补一段与 axios 的signal对照。

环境说明:下文fetch/AbortControllerChrome/Edge/Firefox 近年版本Node.js 18+ 全局 fetch为前提;若你仍在极老环境,请自行引入 polyfill 或换 axios。


一、Promise.race:够用但不等于“取消请求”

1.1 典型写法

functiondelay(ms){returnnewPromise((_,reject)=>setTimeout(()=>reject(newError('timeout')),ms));}asyncfunctionfetchWithRace(url,timeoutMs=8000){constres=awaitPromise.race([fetch(url),delay(timeoutMs)]);returnres;}

1.2 问题在哪?

  • 超时 reject 了,但底层fetch可能仍在进行(取决于实现与时机),你只是“不再 await 它”。
  • 若后续逻辑又触发了多次请求,先发出的慢请求后返回,仍可能造成数据覆盖(竞态)

结论:Promise.race适合作为“超时提示”,但不是语义上的取消


二、AbortController:标准答案(推荐)

2.1 一句话结论

AbortController会通知fetch中止请求(在支持的环境下),从语义上同时解决「超时」与「用户主动取消」。

2.2 最小可用示例

asyncfunctionfetchJson(url,{timeoutMs=8000,signal:outerSignal}={}){constcontroller=newAbortController();consttimer=setTimeout(()=>controller.abort(),timeoutMs);// 如果外层也传了 signal(例如组件卸载),一并监听constonAbort=()=>controller.abort();outerSignal?.addEventListener('abort',onAbort,{once:true});try{constres=awaitfetch(url,{signal:controller.signal});if(!res.ok)thrownewError(`HTTP${res.status}`);returnawaitres.json();}finally{clearTimeout(timer);outerSignal?.removeEventListener('abort',onAbort);}}

要点:

  • fetchsignalAbortController.signal绑定。
  • controller.abort()后,fetch会 reject,错误名通常是AbortError(以浏览器为准)。
  • finally里清理定时器,避免泄漏。

三、封装:超时 + 外层 signal(组件卸载)合并

下面示例不依赖AbortSignal.any(部分旧浏览器无该 API),用「监听外层 abort → 转发到内层 controller」实现合并。

classHttpErrorextendsError{constructor(message,{status,cause}={}){super(message);this.name='HttpError';this.status=status;this.cause=cause;}}exportasyncfunctionfetchWithTimeout(url,options={}){const{timeoutMs=8000,signal:outerSignal,...rest}=options;constcontroller=newAbortController();consttimer=setTimeout(()=>controller.abort(),timeoutMs);constonOuterAbort=()=>controller.abort();outerSignal?.addEventListener('abort',onOuterAbort,{once:true});try{constres=awaitfetch(url,{...rest,signal:controller.signal});if(!res.ok)thrownewHttpError(`HTTP${res.status}`,{status:res.status});returnres;}finally{clearTimeout(timer);outerSignal?.removeEventListener('abort',onOuterAbort);}}

若运行环境已支持AbortSignal.any(较新 Chromium),也可把多个AbortSignal合并成一个再交给fetch上线前务必看目标用户浏览器占比


四、和 axios 对照(如果你项目里仍是 axios)

axios(v0.22+ 推荐写法)同样走signal

importaxiosfrom'axios';constcontroller=newAbortController();axios.get('/api/user',{signal:controller.signal});// 取消controller.abort();

要点:不要再背老的 CancelToken(逐步淘汰),新项目统一AbortController心智。


五、常见踩坑(面试与线上都高频)

5.1 竞态:先请求后返回覆盖新数据

现象:搜索框输入abc,先发出的ab后返回,把列表显示成旧结果。

建议:为每次请求生成递增reqId,或直接用AbortController取消上一次搜索请求。

letsearchController;asyncfunctionsearch(q){searchController?.abort();searchController=newAbortController();constres=awaitfetch(`/api/search?q=${encodeURIComponent(q)}`,{signal:searchController.signal,});returnres.json();}

5.2 React StrictMode 开发环境双重挂载

现象:开发模式下useEffect执行两次,看起来像“请求发了两次”。

建议:这是开发期行为;真正要处理的是cleanup 里 abort

useEffect(()=>{constcontroller=newAbortController();fetch('/api/data',{signal:controller.signal}).then(/* ... */);return()=>controller.abort();},[]);

5.3 把 abort 当“业务失败”

abort()可能是用户离开页面,也可能是超时策略。不要默认弹“请求失败”,先在业务层区分:

  • AbortError/DOMException:多半是取消或超时策略触发
  • HttpError:才是服务端状态问题

六、总结

方案超时真正取消请求(语义)典型适用
Promise.race能提示超时不保证极简 demo
fetch + AbortController能(推荐)浏览器/Node18+

如果你只能记住一句话:要取消,就AbortControllerPromise.race只是计时器。

欢迎大家点赞,关注,一起学习~

http://www.jsqmd.com/news/662531/

相关文章:

  • 别再为竖屏视频发愁!用Premiere一键旋转并适配横屏的完整工作流
  • 从Pwin3.2到Win11:otvdmw如何成为16位程序的‘时光机’?聊聊它的原理与局限
  • GDSDecomp深度解析:Godot游戏逆向工程的架构设计与性能优化
  • 如何用LeRobot在3天内打造你的第一个智能机器人?
  • BetterNCM安装器完全指南:3步解锁网易云音乐插件生态
  • 【进阶篇 / DNS】(7.0) ❀ 02. 多线接入下的DNS策略优化 ❀ FortiGate 防火墙
  • 安装materials studio 2023版本
  • 从XSA到启动卡:Petalinux定制嵌入式Linux系统的全流程实战
  • 本地AI部署硬件之争,为什么Mac Mini和塔式机“都对”却永远吵不完
  • 基于STM32标准库的MS5837驱动移植与IIC时序调试实战
  • 高通SDM660手机开机到Linux内核,ABL的LinuxLoader都干了啥?(代码流程详解)
  • 【注意力机制演进】从SE到CBAM:通道注意力核心思想与代码实战解析
  • 从Bash切换到Zsh后,如何让Kali的渗透测试工具(如Msfvenom)命令补全更丝滑?
  • 别再瞎改retarget.c了!深入理解Keil AC5/AC6/GCC的printf重定向底层差异
  • 3步彻底解决Windows系统卡顿问题:Winhance中文版完全指南
  • 家用路由器当AP用?小心这个坑!详解双路由器组网下的设备互访与防火墙设置
  • ABAP AES加密实战:从标准类库到外部集成的安全方案
  • Arduino IDE安装避坑指南:从下载到中文设置一步到位
  • 从Simulink仿真结果反推:手把手教你读懂Stateflow动作的执行顺序(以5个典型模型为例)
  • DFIG_Wind_Turbine:基于MATLAB/Simulink的矢量控制双馈异步风力发...
  • K8s Pod 卡在 NotReady 状态:深入排查与修复 image filesystem 容量异常
  • CRM 客户管理系统对企业运营效率的提升价值研究
  • STM32+FreeRTOS内存分配全图解:从启动文件到任务栈的硬件级解析
  • PPTTimer:告别演讲超时的智能计时助手
  • 别再手动调参了!用YOLOv5的K-means+遗传算法,为你的数据集定制专属Anchors
  • 【数据结构】栈和链表基本方法的实现
  • 【Unity】Unity C#基础(一)从1.0到9.0:C#版本演进与Unity引擎适配史
  • Grafana 13.0.1 正式发布,带来 Dashboard、Provisioning 功能更新与 Bug 修复
  • 别再踩坑了!Ubuntu 20.04/22.04下禾赛Pandar系列激光雷达ROS驱动保姆级安装指南
  • .NET金融数据集成终极指南:如何快速获取Yahoo Finance股票数据