恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

深入拆解Promise:从状态机原理到微任务与并发控制实战

  • 首页
  • 资讯中心
  • /
  • 深入拆解Promise:从状态机原理到微任务与并发控制实战

相关资讯

拉扎维模拟CMOS课后题详细版答案:从符号约定到推导复现的完整指南 2026/10/15 22:01:50
10个Python自动化脚本:从文件整理到办公提效 2026/10/15 22:01:50
工具、测试与部署:打造一条顺滑的软件交付链路 2026/10/15 22:01:50

最新资讯

ViTPose 中基于 ResNeXt 骨干的 MPII 人体姿态估计:配置解析、源码实现与训练实战
Harvester 项目中的 go-containerregistry remote 包:面向 OCI 镜像仓库的完整客户端指南
WebLogic补丁安装实战:OPatch应用、验证与滚动升级全解析
深入理解 Ginkgo v2:用 BDD 风格的 DSL 编写高表达力的 Go 测试套件
fountain 日志模块 f_log 实战指南:LoggerFactory、Appender 与异步落盘原理
神经网络架构搜索(NAS)实战指南:搜索空间、策略与评估三要素

今日推荐

AI视频生成的时间一致性难题与时空建模演进
基于bbox的PDF文档解析:多栏重排与水印过滤实战
探矿数据进RAG前的清洗全攻略:TXT、Word、PDF、网页四类文档处理实践

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

深入拆解Promise:从状态机原理到微任务与并发控制实战

发布时间:2026/10/15 22:06:51
深入拆解Promise:从状态机原理到微任务与并发控制实战 1. Promise到底解决了什么问题1.1 问题本质不是“回调地狱”而是信任链崩塌很多教程一上来就说Promise解决回调地狱这个说法太浅了。回调地狱只是嵌套代码看起来丑真正要命的是异步调用里的信任问题。我举个实际场景你要在用户点击按钮后先请求用户信息再根据用户信息请求他的订单列表最后用订单列表渲染页面。用回调写是这样的getUserInfo(userId, (user) { getOrderList(user.id, (orders) { renderOrders(orders, (result) { // 还有下一步吗 }); }); });嵌套深只是表象。真正的问题是getUserInfo这个函数回调什么时候执行执行几次如果它内部在成功后又意外调用了回调两次呢如果它抛异常但被吞掉了呢你的页面可能渲染两遍或者卡在加载态不动。回调函数的调用时机、调用次数、异常传播全都不受你控制这就叫信任崩塌。Promise做的事情很简单——把“回调函数”换成了“状态机”。你拿到的永远是一个Promise对象这个对象有且只有三种状态pending进行中、fulfilled已成功、rejected已失败。状态只能从pending变到fulfilled或rejected一旦变了就锁死不可能再回头。这意味着then里的回调最多执行一次状态定了之后你再怎么折腾也不会改变结果。用Promise重写上面那个场景getUserInfo(userId) .then((user) getOrderList(user.id)) .then((orders) renderOrders(orders)) .catch((err) toast(err.message));我常说一句话回调是把未来交给别人Promise是你自己盯着那个状态。这个心智模型的转变才是关键。1.2 状态机是Promise的骨架Promise是个不能再典型的状态机。画出来就是pending初始态既没成功也没失败fulfilled成功态必须携带一个值valuerejected失败态必须携带一个原因reason状态的转移只有两条路pending - fulfilled、pending - rejected。这两条路一旦走完Promise就“定型”了。定型之后你再调用resolve或reject都是无效的。这个设计到底带来什么好处我举几个日常开发里实实在在遇到的场景第一重复调用天然免疫。比如你用一个Promise包装轮询接口第一次成功之后后面的then已经挂上去了你怎么修改promise本身都不影响已经注册的回调执行结果。第二缓存变得极其安全。开发里经常有这种函数同一个请求只要并发一次后面的人都等着复用同一份数据。用Promise做缓存因为状态不可逆所以你返回给十个调用方的都是同一个Promise大家拿到的就是同一份结果绝对不会出现一个人拿到结果另一个人却收到错误的情况。const cacheMap new Map(); function fetchDataWithCache(key) { if (cacheMap.has(key)) { return cacheMap.get(key); } const promise fetch(/api/data/${key}).then((res) res.json()); cacheMap.set(key, promise); return promise; }注意这里缓存的是Promise不是数据结果。这样就算请求还在pending后面再来的人也是等同一个Promise并发请求被天然合并。1.3 关键要点与常见误区Promise的状态不可逆这是理解整个API设计的钥匙。我见过不少同学在项目里写这样的代码let resolveFn; const promise new Promise((resolve) { resolveFn resolve; }); // 某个地方想重置状态 function reset() { resolveFn undefined; // 没用的promise已经定型了 }这是对Promise的误用。Promise设计出来就不是为了让你反复使用的它的核心语义是一次性结算。需要重复触发的异步事件比如用户反复点击某个按钮获取最新数据应该用事件流或者队列来控制而不是试图去重置Promise。另外要记住executor传给构造函数的那个函数是同步执行的。也就是说new Promise(() {})里面的代码会立刻执行。很多人以为Promise里全是异步其实只有then、catch、finally里注册的回调才是异步的。注意Promise构造函数里的同步代码出错会被自动捕获并变成rejected状态不需要你手动try/catch。但executor里如果用了setTimeout里面的异常就不会被Promise捕获了因为Promise早就离开executor了。这个点稍后展开。2. 核心API逐个拆解每个方法背后的取舍2.1 基础的三个消费方法then、catch、finallythen可以传两个参数第一个处理fulfilled第二个处理rejected。但我建议永远只传一个成功回调错误处理统一交给catch。为什么因为then的第二个参数只能捕获当前Promise的错误如果第一个回调里面又抛了异常它自己是接不住的必须往后抛给下一个catch。既然无论如何都需要catch那直接在链尾挂一个逻辑更集中。catch的本质就是then(null, onRejected)但可读性完全不是一个级别。finally则在状态定型之后、无论成功失败都会执行。注意finally的回调不会接收任何参数它拿不到value也拿不到reason它只是“收尾”用的比如关掉loading、清理定时器。有一点容易忽略finally回调返回的值会被忽略但如果finally里抛了异常或者返回了一个rejected的Promise这个异常会继续向下传递覆盖掉前面正常的结果。所以finally里尽量不要放可能出错的代码真要放得自己catch。2.2 静态方法的使用边界日常开发最多用到这几个静态方法我把它们的使用场景放在一起对比Promise.all(iterable)全部成功后拿到结果数组任何一个失败则整体失败。适合并行拉取多个互不依赖的接口。Promise.race(iterable)谁先定型就采用谁的结果不管成功还是失败。适合超时控制。Promise.allSettled(iterable)等所有都定型结果数组里每一项是{status, value}或{status, reason}。适合批量操作需要知道每一次的成败。Promise.any(iterable)只要有一个成功就成功全部失败才算失败。适合从多个备用接口中取最快成功的。这几个方法的边界我踩过坑帮你提前踩平all最大的坑是“一损俱损”。如果三个请求里有一个因为网络波动失败另外两个甚至都还没执行完整个all就直接进入rejected了。很多场景其实不需要这种“全有或全无”你可能更关心哪些成功了哪些失败了那就用allSettled。命名虽然长但语义非常准确。race用来做超时有个隐蔽的问题超时之后原来那个慢请求并没有被取消它还在后台跑着只是它的结果你不再关心了。如果那个请求后面又回调了别的副作用你可能会踩到竞态。所以用race做超时最好给慢请求也挂一个catch兜底避免异常变成unhandledrejection。any是相对较新的API浏览器兼容性已经不错了。它的失败条件比race苛刻需要全部失败才会rejected且rejected的reason是个AggregateError聚合了所有失败原因处理的时候记得取err.errors而不是直接err.message。2.3 日常开发用得最多的组合模式我在项目里常年会封装两类工具函数基本能覆盖大多数异步并联的需求第一类并发请求但容忍部分失败并且需要拿到每次请求的独立结果。用allSettledasync function getBatchData(urls) { const results await Promise.allSettled(urls.map((url) fetch(url))); return results.map((item, index) { if (item.status fulfilled) { return { url: urls[index], data: item.value }; } return { url: urls[index], error: item.reason }; }); }第二类带超时的请求。用racefunction withTimeout(promise, ms) { const timerPromise new Promise((_, reject) { setTimeout(() reject(new Error(请求超时: ${ms}ms)), ms); }); return Promise.race([promise, timerPromise]); }注意我上面给的是reject而不是resolve超时应该认定为失败这样外部才能通过catch捕获并提示用户。如果你用resolve(timeout)调用方就得自己判断返回值非常容易漏掉分支。2.4 一张表看懂选型我给自己做过一张速查表写代码的时候直接对照避免想半天方法判定规则失败时机返回内容适用场景all全部成功才成功第一个rejected成功值数组并行请求整体强一致allSettled全部定型即为完成永不rejected除非传入异常状态值/原因数组批量操作关注每个结果race第一个定型者胜出第一个rejected那个定型者的值/原因超时、竞态淘汰any第一个成功者胜出全部rejected第一个成功的值多个备用来源取最先可用这张表的关键字段就是“失败时机”和“返回内容”。我建议你把这张表贴在手边用错方法的概率能降一半。3. then链式调用的执行细节3.1 为什么then能链式调用Promise从设计上就是为了链式而生的每个then调用都返回一个新的Promise。这个新Promise的状态取决于上一个then回调的返回值。如果是普通值它会作为新Promise的value传入下一个then如果返回的是一个Promise新Promise会“跟随”那个Promise等它定型后再决定自己的状态如果回调里抛了异常新Promise直接变成rejected。这个过程保证了错误可以在链尾统一处理这就是链式调用能持续下去的根本原因。我在讲这部分的时候经常用一个类比接力赛。前一棒跑完把接力棒返回值交给下一棒。如果中间突然摔倒了抛异常比赛停止直接进入医疗站catch。每个then都相当于一个新的起跑线。3.2 什么时候需要手动包一层Promise.resolve有一种情况很典型某个接口在某些条件下需要走异步分支其他条件下可能直接返回缓存值。如果你让then的回调有时候返回普通值有时候返回Promise其实没有问题——Promise机制会自动用Promise.resolve包装普通值。但如果你在函数内部做判断最好统一处理function loadData(id) { const cached cache.get(id); if (cached) { return Promise.resolve(cached); } return fetchData(id).then((data) { cache.set(id, data); return data; }); }这种统一返回Promise的写法好处非常明显调用方完全不用区分同步还是异步直接loadData(id).then(...)就行。实际项目里混合同步和异步的情况非常多统一返回Promise是降低心智负担的最有效手段。3.3 链式写法和async/await的关系很多人以为async/await和Promise是两套东西其实它们就是同一套东西的表层语法。async函数返回的一定是一个Promiseawait是在等待一个Promise的定型。本质上我写app.getData(/user).then((user) app.save(user));和写async function handle() { const user await app.getData(/user); await app.save(user); }是等效的。区别在于async/await让代码看起来像同步但底层的链式关系一模一样。我见过有的团队落地规范时要求“能用async/await就不用then链”其实是不对的。有些场景比如数据流的多次分支、并发编排用then链的组合能力反而更干净async/await强行拆开会增加无用变量和中间态。正确的做法是异步流程简单直接就用async/await需要表达“分支-合并-容错”这类拓扑结构时用原始Promise的组合能力更强。3.4 then链里的错误捕获密度链式调用的错误传播有一条主线从链头到链尾只要有一个环节抛错或者返回了rejected的Promise整个链路会跳过中间所有的then回调一直到遇到catch为止。这个机制非常好但也要注意捕获的范围问题。我见过一个自认为“加了catch已经很稳”的例子fetchData() .then((data) { render(data); }) .catch((err) { toast(err.message); });看起来没问题但如果render里面的某个组件自己抛了一个异常这个异常会被catch捕获并toast出来。此时toast可能提示一个误导性的错误。更合理的写法是分清预期内错误和预期外错误fetchData() .then((data) { try { render(data); } catch (e) { // 渲染异常应该单独记录日志而不是当作请求失败 console.error(render failed, e); } }) .catch((err) { toast(err.message); });关键在于catch不应该是所有异常的安全网它只负责捕获你期望处理的错误。渲染问题属于逻辑错误应该让全局错误监控去关注而不是被业务toast吞掉。4. 手写一个精简Promise把原理吃透4.1 从状态机到then的落地纸上谈兵半天不如亲手写一个。我建议每个前端人都手写过一遍精简Promise不是为了重复造轮子而是理解里面的两个核心机制状态流转和微任务调度。先不管微任务我们先做一个能跑的基础版class MyPromise { constructor(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; const resolve (value) { if (this.state ! pending) return; this.state fulfilled; this.value value; this.onFulfilledCallbacks.forEach((fn) fn()); }; const reject (reason) { if (this.state ! pending) return; this.state rejected; this.reason reason; this.onRejectedCallbacks.forEach((fn) fn()); }; try { executor(resolve, reject); } catch (e) { reject(e); } } then(onFulfilled, onRejected) { if (this.state fulfilled) { onFulfilled(this.value); } else if (this.state rejected) { onRejected(this.reason); } else { this.onFulfilledCallbacks.push(() onFulfilled(this.value)); this.onRejectedCallbacks.push(() onRejected(this.reason)); } } }这个版本能跑通最基本的场景但有个致命问题它没有链式、没有延迟执行、没有值传递。接下来逐步补全。4.2 微任务then回调为什么要异步执行第一版代码里如果Promise已经fulfilledthen回调会立刻同步执行。这在实际使用中会导致一个问题——时序不确定。比如下面这个例子const promise new MyPromise((resolve) resolve(1)); promise.then((v) console.log(first, v)); console.log(second);第一版的输出顺序是first 1 second但原生Promise输出一定是second first 1为什么原生Promise要强制异步因为同步执行会导致代码顺序和调用顺序脱节你没法保证then注册之前Promise是否已经处于fulfilled状态这会造成“有时同步有时异步”的薛定谔时序极其难以调试。强制异步化之后所有then回调都排到当前同步代码之后执行时序就统一了。在浏览器环境实现异步化最合适的工具就是微任务队列。实现上我们可以简单用queueMicrotask或者用Promise.resolve().then等下这里自己实现Promise又用Promise有点套娃但作为教学没问题。我先提供一个比较务实的手写思路function asyncInvoke(fn, ...args) { queueMicrotask(() { try { fn(...args); } catch (e) { // 需要把异常抛到全局否则会被吞掉 setTimeout(() { throw e; }); } }); }把then里所有对onFulfilled/onRejected的调用都换成asyncInvoke时序就和原生Promise一致了。这背后牵出的就是微任务与宏任务的理论放到下一章细讲。4.3 链式返回与值穿透接下来是最核心的部分then必须返回新的Promise并且根据回调返回值的类型决定新Promise的状态。这里涉及Promise Resolution Procedure简单说就是如果回调返回了一个Promise新Promise要跟随它如果返回普通值直接作为新Promise的value如果回调抛异常新Promise直接rejected。一个能表达核心逻辑的then实现then(onFulfilled, onRejected) { const promise2 new MyPromise((resolve, reject) { const handleFulfilled () { try { const result onFulfilled(this.value); resolvePromise(promise2, result, resolve, reject); } catch (e) { reject(e); } }; const handleRejected () { try { const result onRejected(this.reason); resolvePromise(promise2, result, resolve, reject); } catch (e) { reject(e); } }; if (this.state fulfilled) asyncInvoke(handleFulfilled); else if (this.state rejected) asyncInvoke(handleRejected); else { this.onFulfilledCallbacks.push(() asyncInvoke(handleFulfilled)); this.onRejectedCallbacks.push(() asyncInvoke(handleRejected)); } }); return promise2; }resolvePromise的角色是处理“回调返回了Promise”的情况function resolvePromise(promise2, result, resolve, reject) { if (promise2 result) { reject(new TypeError(Chaining cycle detected)); return; } if (result instanceof MyPromise) { result.then( (val) resolve(val), (err) reject(err) ); } else { resolve(result); } }这里有个细节必须提promise2 result的判断。如果你在then里面返回了then返回的那个Promise自己就会形成循环引用的死链必须拒绝。原生Promise的做法是抛TypeError我们手写时也应该模拟。4.4 两个容易出错的细节第一个细节是onFulfilled和onRejected不是必传参数。如果某一步then没有传成功回调值应该被穿透到下一步。所以实现时要做默认值兜底onFulfilled typeof onFulfilled function ? onFulfilled : (value) value; onRejected typeof onRejected function ? onRejected : (reason) { throw reason; };第二个细节是resolve的时候要允许Promise嵌套。比如executor里调用了resolve(anotherPromise)则需要让当前Promise跟随另一个Promise。这个逻辑也走resolvePromise所以在resolver里要加一层检查。完整的手写实现网上到处都是但如果你能不看任何参考自己写出这个版本然后再对着Promise/A规范一条条验证你对Promise的理解会比只读十遍文档深刻得多。这也是我面试别人时最看重的一项能力。5. 微任务与事件循环Promise为什么是这个时序5.1 宏任务和微任务的本质区别要彻底搞懂Promise的执行顺序就必须理解浏览器的事件循环机制。大的执行规则是同步代码先全部执行完然后开始处理微任务队列微任务队列清空后浏览器才会取一个宏任务执行执行完再清空微任务队列……如此往复。宏任务包括setTimeout、setInterval、requestAnimationFrame、用户交互事件、I/O回调等。微任务包括Promise.then/catch/finally、queueMicrotask、MutationObserver回调等。两者的关键区别在于队列清空的时机。每次执行完一个宏任务浏览器都会把微任务队列清空再决定下一个宏任务。微任务里产生的微任务依然会在同一个宏任务周期内被排空。也就是说微任务可以无限“插队”宏任务只能等到所有微任务清空才轮到它。我用一个生活化的类比事件循环是一个机场宏任务是国际航班微任务是安检通道。每次国际航班落地后必须先让所有安检通道的乘客走完下一趟国际航班才能降落。如果安检通道里不断出现新的乘客微任务里继续产生微任务那国际航班就只能一直等着。5.2 一道经典的执行顺序题我们来看一道前端面试几乎必出、实际开发也经常用到的题目先不要看答案自己在心里跑一遍console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); Promise.resolve() .then(() { console.log(promise1); }) .then(() { console.log(promise2); }); console.log(script end);正确的输出顺序是script start script end promise1 promise2 setTimeout原因拆解如下同步代码执行先打印script start。遇到setTimeout把它放入宏任务队列。遇到Promise.resolve().then把第一个then的回调放入微任务队列。打印script end。此时同步代码全部执行完毕。清空微任务队列执行第一个then打印promise1。这个then执行后又返回一个Promise第二个then被注册并继续排入微任务队列所以紧接着打印promise2。微任务清空事件循环取出宏任务队列里的setTimeout回调打印setTimeout。这个题目之所以容易答错是因为很多人以为setTimeout(0)应该立即执行。其实哪怕setTimeout写的是0毫秒它也要等到它对应的宏任务周期开始才会执行而那个周期永远排在微任务清空之后。5.3 微任务无限制递归会饿死渲染在实际开发里有一个真实存在的风险微任务无限递归。比如一段逻辑自己给自己追加thenfunction loop() { Promise.resolve().then(() loop()); } loop();这会导致微任务队列永远被新任务填满渲染进程根本没有机会执行宏任务页面会直接卡死。你在开发中如果用Promise去做轮询或者递归遍历一定要留退出条件否则体验就是黑屏。反过来如果这个递归用setTimeout来写每次递归至少会经过一个宏任务的间隙浏览器还有机会渲染画面、响应用户输入。这也是为什么有些动画循环用setTimeout而不用Promise的原因。提示理解微任务排队规则在排查“页面明明数据变了却不刷新”这类问题时有奇效。有些状态更新发生在微任务里如果微任务链没走完页面渲染就迟迟不触发UI表现就是卡顿或失灵。6. 常见问题与排查技巧实录6.1 unhandledrejection被遗忘的失败前端最普遍的一个Promise问题是rejected的Promise没有被处理导致浏览器抛出unhandledrejection。Debug难度在于它不会立刻报错而是在本轮事件循环结束时才触发。常见的原因有这么几种用Promise.race做超时慢请求在超时之后才rejected但race已经返回了调用方的catch绑定的是race返回的Promise原来的promise没人接住错误。在async函数里await没有套try/catch函数外部调用时又没.catch异常直接悬空。动态添加的Promise没有挂catch。排查工具上用浏览器DevTools的Console面板开启“Log unhandled rejections”可以定位到具体的promise创建位置。但更推荐在应用里主动监听window.addEventListener(unhandledrejection, (event) { console.error(未处理的Promise拒绝:, event.reason); // 上报到监控平台 });预防比排查更重要我在团队里定的规范是任何非全局的Promise要么被await要么显式调.catch禁止“裸奔”。代码评审时看到裸的somePromise()而没有接结果直接打回。6.2 Promise.resolve的执行顺序被误解有一个日常很容易写错的地方Promise.resolve(x).then(...)和new Promise(resolve resolve(x)).then(...)在语义上没有区别但时序上和直接调用then不同。看这个例子const p Promise.resolve(1); p.then((v) console.log(a, v)); p.then((v) console.log(b, v));输出一定是a、b因为同一个Promise上注册的then按注册顺序排队执行。但如果你分别在两轮微任务之后再去读p的值结果一样顺序却可能不同。这里要记住的是多次then同一个Promise是合法的每次then都会创建一个独立的新分支互不干扰。6.3 一直pending的Promise导致的内存泄漏Promise本身不会泄漏但如果你创建了一个永不定型的Promise并且它被一个长期存活的对象引用着那它上面挂着的回调闭包也会一直被引用内存就泄漏了。一个典型的场景是进度条组件组件在挂载时创建了一个await waitForSomeEvent()但事件永远不会触发组件却已经销毁了。这时候await下面的代码永远不执行闭包却还被Promise内部队列引用着。解决方式是给异步流程增加超时或取消能力function cancellablePromise(promise, signal) { return new Promise((resolve, reject) { const onAbort () reject(new Error(aborted)); signal.addEventListener(abort, onAbort); promise.then( (val) { signal.removeEventListener(abort, onAbort); resolve(val); }, (err) { signal.removeEventListener(abort, onAbort); reject(err); } ); }); }配合AbortController可以在组件卸载时主动放弃避免回调悬挂在内存里。6.4 并发控制限制同时进行的请求数量Promise.all会把所有请求同时发出去但真实场景里接口并发是有上限的尤其在批量上传、批量拉取头像、爬取数据时10个、20个并发还扛得住一次上百个请求直接打爆服务端或者触发限流。所以并发控制是每个前端都要掌握的技能。我用队列实现一个简单的并发池async function mapWithConcurrency(tasks, limit, handler) { const results new Array(tasks.length); let index 0; async function worker() { while (index tasks.length) { const currentIndex index; const task tasks[currentIndex]; try { results[currentIndex] await handler(task); } catch (e) { results[currentIndex] e; } } } const workers Array.from({ length: Math.min(limit, tasks.length) }, () worker()); await Promise.all(workers); return results; }这个实现的思路是固定起limit个工人每个工人不停地从任务队列里取出下一个任务执行执行完继续取。每个任务的结果和异常都被单独保存不会因为一条失败导致整体中断。实际项目里我把这个函数放进了工具库批量处理图片压缩、文件上传、分页拉数据都用它稳定得很。另一个需要注意的点是并发数不是越大越好。一次发起的请求过多浏览器对同一域名的连接数本身就是有限的超出部分会排队。合理设置limit一般4到6就已经能跑满大多数场景不需要追求极限。6.5 fetch超时与请求取消的完整方案讲完并发控制顺手把fetch超时这个高频需求也讲透。前面用race可以做超时但真正的请求并没有被中断。现在fetch配合AbortController可以真正取消请求function fetchWithTimeout(url, options {}, timeout 10000) { const controller new AbortController(); const timer setTimeout(() controller.abort(), timeout); return fetch(url, { ...options, signal: controller.signal }).finally(() { clearTimeout(timer); }); }使用上调用方可以通过try/catch捕获AbortErrortry { const res await fetchWithTimeout(/api/user, {}, 3000); const data await res.json(); } catch (e) { if (e.name AbortError) { // 用户看到的提示应该是“请求超时”而不是“网络错误” } else { // 其他错误 } }这里finally清定时器是个容易遗漏的细节。如果你不清timer即使请求提前成功了定时器还会在后台悬挂等到触发abort时再来一次无意义的abort虽然不会报错但白跑一个宏任务。细节积累多了代码质量的天梯就是这样一步一步爬上去的。6.6 最终的经验分享我在实际项目里写Promise踩过最深的坑就是await后面不套try/catch以为async函数会自动处理异常。如果你的页面上有一个await失败的接口没有被捕获整个函数之后的代码全部不执行用户看到的是一块空白页面而控制台的报错又非常隐蔽。后来我们统一封装了请求层所有业务await只负责拿数据异常全部在请求层集中处理并提示代码干净了不止一个量级。另外一个深刻的经验是JSON.parse放在then里如果返回的数据格式不对它会抛SyntaxError。这个错误如果被Promise链的catch捕获提示信息会是“解析失败”而不是网络问题。建议在解析数据时单独用工具函数并给出明确错误别什么都丢给通用catch。最后我想说Promise是前端异步编程的基石但地基之下还有事件循环、宏任务微任务、渲染时机这些更深的层次。手写一遍Promise、跑一遍执行顺序题、给自己的项目加一个并发池这三件事做完你对Promise的掌控力会上一个明显的台阶。前面说的那些坑你大概率都能绕开。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号