ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript Promise 底层原理与工程实践:从回调地狱到并发控制

JavaScript Promise 底层原理与工程实践:从回调地狱到并发控制 1. 回调地狱的真实面目嵌套只是表象失控才是本质1.1 一段能让人瞬间想起噩梦的经典代码我入行第三年的时候接了一个后台管理系统的订单模块。需求本身不难用户下单前要依次检查登录态、拉取用户信息、读取购物车、计算优惠、创建订单。如果用当时的标准姿势写大概长这样checkLogin(function (loginResult) { if (!loginResult.success) { showError(登录已过期); return; } fetchUserInfo(function (userInfo) { fetchCart(userInfo.id, function (cart) { calculateDiscount(cart, function (discountInfo) { createOrder(userInfo, cart, discountInfo, function (order) { goToPayPage(order); }, function (orderError) { handleOrderError(orderError); }); }, function (discountError) { handleDiscountError(discountError); }); }, function (cartError) { handleCartError(cartError); }); }, function (userError) { handleUserError(userError); }); });这不是我编出来的夸张写法这就是当时团队里的真实代码。每加一个异步环节嵌套就深一层缩进就往右推一截。等注释和异常处理混进去之后整个函数几乎没法从头读到尾。你要问这段代码到底在干嘛得先花五分钟把每一层括号配对清楚。标题里提到的回调地狱英文叫 Callback Hell说的就是这种异步操作层层嵌套导致的代码失控状态。ES6 引入的 Promise 之所以被当成异步编程的转折点不是因为它消灭了回调而是它把回调里套回调的横向蔓延改成了.then()链的纵向罗列。结构一平铺脑子里的负担立刻降下来。1.2 嵌套背后真正的问题流程控制、错误传播、维护成本很多人以为回调地狱只是丑。单说丑那忍忍也就过去了。真正要命的是三个连带问题。危害一流程控制一塌糊涂。上面的代码是纯串行的每一步都依赖上一步的结果。实际业务里还有更恶心的场景两个接口并行都跑完再做下一步三个接口有一个失败就整体回滚重试三次仍然失败才提示用户。这些逻辑如果用回调写每一种都要自己造轮子——计数器、标记位、定时器全部手搓搓完还不一定对。危害二错误处理形同虚设。回调风格里每个异步函数都要接收一个 error 参数或者 error 回调你就得在每个层级里手动判断、手动传递。我见过最离谱的代码错误处理分支比正常分支还多review 的时候根本没人能确认每条错误路径都覆盖全了。一旦某个回调忘了处理错误报错信息直接淹没在控制台你还得靠二分法打断点排查。危害三维护成本爆炸。需求变更的时候要在嵌套中间插一个新的异步步骤意味着后面所有的代码都得整体往右移一层缩进重排括号重对。改完还要担心有没有不小心动到别人改过的逻辑。这种代码放两三个月再看原作者都得愣住更别提新接手的人。回调本身不是原罪异步编程必须有某种之后再做的机制。问题在于回调把什么时候继续这个控制权交给了每个函数自己没有一个统一的编排者。Promise 做的事情本质上就是把控制权收拢——状态一旦确定后续流程就由 Promise 机制接管不用每个回调自己操心下一步。1.3 Promise 解决的不只是格式问题更是流程控制问题Promise 不是 ES6 凭空发明的它背后的思想在 1976 年就有人提出了后来在 CommonJS 社区里演化出多种实现直到 ES6 把它标准化成语言内置对象。它的核心模型非常简单一个 Promise 对象代表一个异步操作的结果这个操作只会处于三种状态之一——等待中pending、已成功fulfilled、已失败rejected。状态一旦从 pending 离开就不能再变。这个只变一次的特性是 Promise 所有好用的基础。有了这个对象异步代码的写法就从下一步交给回调变成了下一步通过返回 Promise 来编排。你不需要在回调函数里手写成功之后做什么失败之后做什么只需要声明式地调用 .then() 和 .catch()。至于这个 Promise 什么时候结束、以什么状态结束是异步操作内部的事情外部代码通过链式调用跟着节奏走就行。我在实际项目里最大的感受是Promise 真正解决的是把人脑要跟踪的异步流程变成了Promise 链路自动管理。我们只需要关心每一段的输入输出不用再关心每一段之间靠什么黏合。这个思维转变才是告别回调地狱的起点。2. Promise 底层机制拆解状态机、微任务与 then 链的联动逻辑2.1 三种状态与一次转移Promise 状态机的设计哲学Promise 的状态规则只有一句话pending 可以变为 fulfilled 或 rejectedfulfilled 和 rejected 不可再变。这个设计看起来简单背后的哲学值得琢磨。想象一下现实中的快递下单后是待揽收揽收后是运输中签收后是已签收。已签收不可能退回运输中更不可能变成已拒收之外的其它状态。Promise 的不可逆条款保证了一个异步操作的结果是稳定的所有依赖这个结果的动作都只会在结果确定之后执行一次不会出现回调被调用两遍这种并发编程里最头疼的问题。再看执行时机。Promise 构造函数接收一个 executor 函数这个函数会立即同步执行const promise new Promise((resolve, reject) { console.log(executor 立刻执行); setTimeout(() { resolve(完成); }, 1000); }); console.log(这行先打出来);注意上面的输出顺序先打印executor 立刻执行再打印这行先打出来。也就是说executor 不是异步的异步的是最终 resolve 的时机。这一点经常被初学者忽略放进 Promise 里的代码并不会自动延迟执行只有 resolve/reject 之后的 .then/.catch 回调才进入异步队列。2.2 then 方法返回新 Promise链式调用的真正秘密很多人第一次学 Promise 都会困惑为什么 .then() 后面还能继续 .then()难道每个 .then() 都返回一个新的 Promise 吗是的准确说.then() 一定返回一个新的 Promise 对象。这个新 Promise 的状态取决于回调函数 result 的返回值如果回调返回一个普通值新 Promise 直接以 fulfilled 状态携带这个值如果回调返回一个 Promise新 Promise 会跟随这个 Promise 的状态如果回调抛出一个异常新 Promise 以 rejected 状态携带这个异常这个机制是链式调用的基石。第一层 .then() 拿到异步结果后如果你想再做一次异步查询直接在回调里返回一个新的 Promise 就行。下一个 .then() 会等待这个新 Promise 结束时再触发。整个过程是自动的不需要手动嵌套。fetchUserInfo() .then((user) { // 这一步返回 Promise下一步会等它完成 return fetchOrders(user.id); }) .then((orders) { // 这里的 orders 是 fetchOrders 的异步结果 console.log(orders); }) .catch((err) { // 上面任何一环抛出错误都会统一汇聚到这里 console.error(err); });这段代码用回调写就是两层嵌套用 Promise 写变成一条平铺的链。而且错误处理只有一个 .catch()不管哪一步出错都会顺着 Promise 链往下传递直到被 catch 捕获。这个错误穿透能力是回调写法很难做到的事情。2.3 微任务队列为什么 Promise 回调先于 setTimeout 执行我刚开始学习 Promise 时做过一个很有意思的测试setTimeout(() { console.log(setTimeout); }, 0); Promise.resolve().then(() { console.log(promise); });输出结果是先打印 promise再打印 setTimeout。原因在于浏览器事件循环里存在两个队列宏任务队列macrotask queue和微任务队列microtask queue。setTimeout 的回调属于宏任务Promise 的 .then 回调属于微任务。事件循环每执行完一个宏任务会清空所有排队中的微任务然后才取下一个宏任务。这在实战中有什么意义它决定了 Promise 回调的相对时序。你在一个 Promise 链里面混合使用 setTimeout、事件监听、requestAnimationFrame 时如果对执行顺序有要求必须意识到微任务和宏任务之间的优先级差异。举个常见的场景在 Vue/React 里数据更新之后想在 DOM 更新完成后再做点什么很多人会嵌套 setTimeout实际上用 Promise.resolve().then() 可以更早更稳地拿到更新后的状态。微任务机制还让 Promise 链具备了确定性链上的每一步都不依赖随机的宏任务调度而是在当前宏任务阶段末尾尽快执行。这大大方便了异步流程的推理——你只要把 Promise 链上的逻辑按顺序读执行顺序基本就是代码顺序不会被 setTimeout 之类的东西乱入打乱。3. 手写 Promise 实战从骨架到链式闭环彻底弄懂底层3.1 构造函数骨架状态与值的存储理解 Promise 最好的方式不是背 API而是自己写一个简版。我建议所有前端开发者至少手写一次 Promise写完以后再看源码、再看 .then 的链式行为都会有豁然开朗的感觉。先搭骨架function MyPromise(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 (err) { reject(err); } }这里有两个细节值得注意。第一executor 执行时如果抛出异常构造函数要负责捕获并把这个异常当成拒绝原因传给 reject。第二数组回调的保存是为了应对executor 异步 resolve的情况——Promise 已经创建但还没出结果此时 .then() 注册的回调必须被存起来等状态确定后再批量执行。3.2 链式闭环then 返回 Promise 并完成值传递接下来是关键实现 .then()让它返回一个新 Promise并处理好回调和返回值之间的关系。MyPromise.prototype.then function (onFulfilled, onRejected) { // 默认值处理让 .then().then() 这种空调用能透传值 onFulfilled typeof onFulfilled function ? onFulfilled : (value) value; onRejected typeof onRejected function ? onRejected : (reason) { throw reason; }; const self this; const handleCallback (callback, resolve, reject) { try { const result callback(self.value); if (result instanceof MyPromise) { // 如果回调返回的是一个 Promise新 Promise 要跟随它的状态 result.then(resolve, reject); } else { resolve(result); } } catch (err) { reject(err); } }; const promise2 new MyPromise((resolve, reject) { if (self.state pending) { self.onFulfilledCallbacks.push(() { handleCallback(onFulfilled, resolve, reject); }); self.onRejectedCallbacks.push(() { handleCallback(onRejected, resolve, reject); }); } else if (self.state fulfilled) { setTimeout(() handleCallback(onFulfilled, resolve, reject), 0); } else { setTimeout(() handleCallback(onRejected, resolve, reject), 0); } }); return promise2; };这个简版里我用 setTimeout 模拟了异步调度。实际 Promise 用的是微任务但核心思路一致回调不会同步执行而是注册到任务队列里等当前栈清空再跑。你在自己手写时把 setTimeout 换成 queueMicrotask 就更接近原生了。处理同步回调时这段代码有个比较容易踩的坑如果把 handleCallback 直接同步调用那么在读取 self.value 时它可能还是 undefined因为 resolve 还没执行。所以我在非 pending 状态也加了 setTimeout 等一步保证回调一定在状态确认之后执行。这个细节和原生 Promise 的回调必定异步触发语义是一致的。3.3 错误穿透与静态方法补齐手写版缺口上面 .then() 里有一段默认处理——当 onFulfilled 不是函数时用(value) value兜底当 onRejected 不是函数时用(reason) { throw reason; }兜底。这个设计就是 Promise 链能错误穿透的原因。比如promise.catch(fn)本质上就是.then(null, fn)当上一个 Promise 出错时错误会沿着链一直往下找最近的 onRejected中间如果没有 catch错误会一直传到底部最终表现为 unhandled rejection。下面补上 catch 和 resolve 静态方法MyPromise.prototype.catch function (onRejected) { return this.then(null, onRejected); }; MyPromise.resolve function (value) { if (value instanceof MyPromise) { return value; } return new MyPromise((resolve) resolve(value)); };写完这些你再用简单用例测一下new MyPromise((resolve) { setTimeout(() resolve(1), 100); }) .then((n) { console.log(n); // 1 return n 1; }) .then((n) { console.log(n); // 2 });如果能依次打印 1 和 2恭喜你你亲手搭建了一条可用的 Promise 链。3.4 手写 Promise 的测试与认知收获我还建议在测试环节故意制造几种边界情况在 executor 里同步 throw 一个 Error看 catch 是否能捕获在 .then 回调里返回一个 rejected 状态的 Promise看链尾的 catch 是否能收到同一个 Promise 调用两次 .then看两个回调是不是都能触发每一个边界情况背后都对应着 Promise 规范里的一个硬性要求。做完这套测试你对Promise 状态不可变回调异步触发错误自动传播这几个概念的印象会比读十篇文章都深。这也是我在团队里带新人时推荐的学习路径别急着背 API先手写一个 50 行的简版 Promise所有抽象概念会瞬间落到代码上。4. 并发编排与错误处理的工程实践4.1 Promise.all、race、allSettled 的选型对照Promise 链解决的是串行问题但业务里经常出现多个异步操作同时跑统一汇总结果的需求。ES6 自带 Promise.allES2020 又加入 allSettledES2021 有 any这些方法我之前在实际项目里的选型逻辑是这样的const results await Promise.all([ fetchUserInfo(), fetchUserOrders(), fetchUserCoupons() ]);Promise.all 的特点是全成才行只要有一个失败整个 Promise 立刻进入 rejected 状态其它结果全部丢失。它适合关键路径上的并行请求——比如支付页需要同时拉取账户余额、支付渠道列表、风控参数任何一个失败都该中断流程。但有些场景不希望一损俱损。比如管理后台首页要展现多个统计卡片其中一个统计接口挂了不应该让整个首页白屏。这时候就要用 Promise.allSettledconst results await Promise.allSettled([ fetchOrderStats(), fetchUserStats(), fetchRevenueStats() ]); // results 数组里每项都是 { status: fulfilled, value } 或 { status: rejected, reason }allSettled 会等所有 Promise 都结束无论成还是败再把结果汇总给你非常适合部分失败可容忍的降级场景。Promise.race 则是谁先到用谁——常用于超时控制把真正请求和定时器赛跑谁先出结果谁生效。但要注意 race 不会取消掉那些落败的 Promise它们的结果依然会在背后执行完所以别用它做资源清理类的工作。4.2 unhandled rejection 的常见来源与排查方法热词里有一条很典型的报错Unhandled Promise Rejection TypeError: WebAssembly.instantiate()以及uncaught (in promise) Error: a listener indicated an asynchronous response by returning a Promise, but that it rejected。这类报错基本都指向同一个问题Promise 链中出现了 rejected 状态但整条链上没有任何一个 .catch() 或 try/catch 去接管它。在 DOM 事件监听器、Chrome 扩展程序的 message listener、WebAssembly 实例化这类场景中特别高发因为这些 API 往往要求回调返回 Promise 表示异步完成而你如果漏写了异常分支浏览器就会把 rejected 的 Promise 抛到全局。我排查这类问题有一个固定套路。第一步打开浏览器控制台找到完整的堆栈栈帧定位是哪一段代码产生的 Promise。第二步在该 Promise 链尾部补一个 .catch() 或 await 外面套 try/catch先让错误不再溢出到全局。第三步在 catch 里把错误对象完整打印出来必要时在关键环节加一个临时的 .tap 观察值。第四步根据错误 message 判断根因——WebAssembly.instantiate 报错大概率是模块字节流有问题而 listener 报错说明你的异步监听函数里有一段 Promise 被 reject 了却没被捕获。开发阶段还应该开着unhandled 即视为错误的严格模式。Chrome/Vue CLI 都有这个选项开发环境一旦出现未处理的 rejected 就立刻抛出而不是默默吞掉这样能倒逼你写出完整的分支处理。4.3 async/await 与 Promise用最舒服的方式组合async/await 不是 Promise 的替代品它是建立在 Promise 之上的语法糖。await本质上就是在等待一个 Promise 的状态变化然后取出 fulfilled 的值或者抛出 rejected 的错误。理解了这层关系你就不会纠结该用 Promise 还是 async/await了。async/await 比裸 Promise 链更贴近人类思维代码长成同步的样子顺序一目了然。但它也有自己的语法陷阱。同步风格的回调里很容易写出串行等待的性能损耗三个没有依赖关系的异步请求如果你写成const a await fetchA(); const b await fetchB(); const c await fetchC();那就是老老实实地一个一个等总耗时约等于三次请求之和。而使用 Promise.all 并行发起总耗时约等于最慢的那个请求。很多性能问题就是这么故意制造出来的。我自己的习惯是有依赖关系、后一步依赖前一步结果的用链式或 async/await 串行没有依赖关系的并行请求一律 Promise.all 加失败兜底。这样既能保证可读性又能压榨并发性能。5. 真实重构案例三层嵌套回调改造成优雅异步流程5.1 重构前的代码与痛点清单为了讲清楚改造思路我准备一个完整的实战案例。场景是登录后进入首页需要依次做三件事先拿用户基础信息再根据用户类型拉取菜单配置最后读取个人消息数。这是非常典型的串行依赖接口流程。改造前的原型function loadHomePage(callback) { fetch(/api/user/info, function (userInfo) { if (!userInfo) { return notifyError(用户信息加载失败); } fetch(/api/menu?type userInfo.userType, function (menu) { if (!menu) { return notifyError(菜单加载失败); } fetch(/api/message/count, function (msgCount) { if (msgCount null) { return notifyError(消息数加载失败); } renderHome(userInfo, menu, msgCount); callback callback(); }); }); }); }这段代码的痛点非常明确三件事三层缩进每个环节都要手动判断返回值的合法性而且判断失败的逻辑散落在各处想加一个新的依赖步骤得再往里面嵌一层。如果你在代码评审里看到类似写法基本可以确定维护它的人每年要损耗很多时间在提心吊胆地改括号里。5.2 第一轮重构先用 Promise 链理顺依赖关系第一轮改造我们不急着上 async/await先把接口函数都包装成返回 Promise 的形式function fetchUserInfo() { return fetch(/api/user/info).then(response { if (!response.ok) throw new Error(用户信息加载失败); return response.json(); }); } function fetchMenuByType(userType) { return fetch(/api/menu?type userType).then(response { if (!response.ok) throw new Error(菜单加载失败); return response.json(); }); } function fetchMessageCount() { return fetch(/api/message/count).then(response { if (!response.ok) throw new Error(消息数加载失败); return response.json(); }); }然后主流程变成function loadHomePage() { fetchUserInfo() .then((userInfo) { return fetchMenuByType(userInfo.userType).then((menu) ({ userInfo, menu })); }) .then(({ userInfo, menu }) { return fetchMessageCount().then((msgCount) ({ userInfo, menu, msgCount })); }) .then(({ userInfo, menu, msgCount }) { renderHome(userInfo, menu, msgCount); }) .catch((err) { notifyError(err.message); }); }第一轮改造的意义在于错误处理从每个节点单独判断变成了整链统一的 catch。同时每一步的返回值被显式地交给下一步数据流变得清晰了。不过这个写法还残留了一个问题——为了把前面的 userInfo 和 menu 传到后面的步骤我不得不用对象一层层手动打包代码还是有一点包袱味。5.3 第二轮重构async/await 回归线性写法第二轮改造目标只有一个让异步代码看起来像同步代码。async function loadHomePage() { try { const userInfo await fetchUserInfo(); const menu await fetchMenuByType(userInfo.userType); const msgCount await fetchMessageCount(); renderHome(userInfo, menu, msgCount); } catch (err) { notifyError(err.message); } }对比一下第一轮和第二轮的差异。第一轮虽然错误集中了但数据传递还要靠对象拼装第二轮每个中间值都是本地变量读代码的时候顺着往下读就行。人的肉眼天然擅长阅读线性的代码不擅长在括号里跳来跳去。这就是 async/await 最大的价值。但要注意一个细节这三个步骤之间有依赖关系吗fetchMenuByType 需要 userInfo.userType所以第一步和第二步必须串行。但 fetchMessageCount 不依赖前两者理论上可以并行执行。如果消息数接口很慢串行写法会白白多等一个请求的耗时。更优的方案是把第二步和第三步改成并行async function loadHomePage() { try { const userInfo await fetchUserInfo(); const [menu, msgCount] await Promise.all([ fetchMenuByType(userInfo.userType), fetchMessageCount() ]); renderHome(userInfo, menu, msgCount); } catch (err) { notifyError(err.message); } }这么一改总耗时从三个请求串行降为第一步 第二三步并行的最大值。在真实业务里这种细微的并发优化往往比加缓存、加 CDN 来得更快更直接。5.4 改造收益复盘与注意点重构之后代码行数没有减少很多但维护体验完全不同。以前加一个新依赖比如拉取用户收藏夹我会在两个位置新增一个接口函数和一个 .then 链节点。改完 Promise 链后只要在 Promise.all 里加一个元素在渲染参数里加一个变量就行。还有一个容易被忽略的收益可测试性。接口函数被拆成一个个返回 Promise 的独立函数后单元测试可以直接 mock 掉每个函数不需要真的发起网络请求。回调版本要测的话得把整个 loadHomePage 塞进测试环境Mock 三个不同的全局 fetch费时费力。这个案例里有个常见误区值得单独提出来很多人重构时把函数体里所有的回调都换成 Promise但接口之间其实没有梳理依赖关系结果只是换了层皮嵌套变成了一堆 then 加对象手动传参。真正的重构核心是数据流——先画出每一步依赖哪些上游数据哪些可以并行然后再动代码。数据流理清了怎么写都不会太差。6. 日常开发中最容易踩的 Promise 坑附自查清单6.1 忘记 return 导致的并发失控这是 Promise 链里最经典的坑之一。有些人写 .then 的时候把异步操作发出去但忘了 returngetUserInfo().then((userInfo) { getOrders(userInfo.id).then((orders) { renderOrders(orders); }); });从功能上看这段代码确实能在拿到订单后渲染列表看起来没毛病。但它打断了 Promise 链——getOrders 返回的 Promise 没有人接管后面的 then 无法等待它。一旦你在 getOrders 之后又加了 .then 想做后续操作新 added 的 then 会拿到的其实是 undefined而不是 getOrders 的结果。更隐蔽的问题出在错误处理。getOrders 如果失败它的 rejected 状态不会被链尾的 catch 捕获因为这条链根本没接上它于是产生 unhandled rejection。排查的时候你会看到错误报在某个完全没印象的函数里非常折磨人。我的经验是在 Promise 链里只要某个回调的意图是继续这个流程就必须在箭头函数里写 return。养成这个肌肉记忆能避开九成 Promise 链断层问题。6.2 在普通函数里给 Promise 赋值常见的初学者翻车点网络热词里有一条很有意思promise 在普通函数里面赋值。很多初学 JavaScript 的同学会写出这样的代码function loadData() { let data; fetchData().then((result) { data result; }); return data; // 这里拿到的永远是 undefined }问题在于 fetchData().then 里的回调是异步触发的而 return data 是同步立刻执行的。当函数返回时data 根本还没被赋值。这个误区和异步代码里不能同步取返回值是同一个道理。正确做法有两种。第一种是把整个函数改成 asyncasync function loadData() { const data await fetchData(); return data; }第二种是直接返回 Promise让调用方决定后续怎么处理function loadData() { return fetchData(); }我在评审新人代码的时候这种模式见过不下十次。它本质上还是对回调延迟执行的理解不够深刻——以为 .then 里的代码会在下一步同步执行实际早就排到任务队列后面去了。建议遇到这种困惑时在脑子里过一遍事件循环同步代码先执行完Promise 回调和异步代码才在下一轮执行。理解了执行顺序就不会再犯这个错。6.3 滥用 Promise.all 带来的资源问题前面我夸了 Promise.all但在有些场景下也不能乱用。比如文件上传、数据库批量写入、大量图片压缩这类有资源上限的操作一次性 Promise.all 并发几十上百个任务很容易把内存打爆或者把后端接口拖垮。我自己遇到过的例子是前端批量导出 PDF用户选了 200 张单据前端要逐个请求后端生成 PDF 再下载。如果直接用 Promise.all200 个并发请求同时发出去后端直接超时告警。后来我不得不写了一个并发池把并发数限制在 5 到 8 个跑一批再跑下一批async function runWithConcurrency(tasks, limit) { const results []; const executing new Set(); for (const task of tasks) { const p Promise.resolve(task()); results.push(p); executing.add(p); const clean () executing.delete(p); p.then(clean, clean); if (executing.size limit) { await Promise.race(executing); } } return Promise.all(results); }这段代码的核心是用 Promise.race 监听最快完成的那一个只要完成一个就腾出位置放新的任务进去。它比最简单的 forEach 并发要稳得多也远没有想象中复杂。记住Promise.all 适合并发量可控的场景一旦任务数量不确定先做并发限流再谈优雅。6.4 一份自查清单最后给大家整理一份我每次写异步代码都会过一遍的清单代码里是否出现了回调套回调超过两层如果是考虑用 Promise 链或 async/await 梳理。每个 .then() 的回调里是否都 return 了下一步要用的 Promise 或值整条链的末端是否有 .catch() 或者 try/catch 兜底任何可能 rejected 的 Promise 都必须有接管者。多个无依赖的异步操作是否被错误地串行等待如果是改成 Promise.all 或 allSettled。有没有在普通函数里同步读取异步赋值如果有把函数改成 async 或返回 Promise。并发任务的数量是否超过了后端或本机的合理上限如果是加并发池。开发环境下有没有打开 unhandled rejection 的严格警告模式保证错误不被静默吞掉。这张清单不复杂但每一条背后都对应着真实项目里踩过的坑。把这些坑都躲过去Promise 基本就算用熟了。我在带项目的时候经常对组员说一句话Promise 不是必须用的银弹但它确实把 JavaScript 异步编程从一个靠自觉维护的状态变成了一种有结构、可分析的工程实践。真正理解它的状态机和链式逻辑之后不管以后是写浏览器脚本、Node 服务、小程序还是 Taro/React Native遇到异步流程都会有底气。回调地狱不是某种宿命只要工具用对了代码完全可以做到又直白又可靠。
返回列表