尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
前端精读周刊:async/await 是把双刃剑——顺序 await 的并发陷阱与性能优化
文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本篇技术指南源自前端精读周刊对 Aditya Agarwal《Avoiding the Async/Await Hell》一文的精读。文章以现代前端常见的顺序await代码为切入点剖析 async/await 作为语法糖带来的隐形串行化问题语法变清爽了异步并发却被悄悄牺牲性能直接传导到用户体验。读完本文你将掌握先发起、后等待的并行写法、Promise.all的正确姿势以及回调、async/await、Promise 三种异步形态各自的适用边界从而在依赖链与并发场景中做出有据可依的选择。1 引言async/await 终于也被吐槽了在 async/await 大行其道的年代很少有人质疑它的副作用。Aditya Agarwal 却认为async/await 语法让开发者陷入了新的麻烦之中——它把异步问题从写法丑变成了性能差。笔者也早就觉得哪里不对劲终于有人把实话说了出来async/await 可能会带来麻烦。本篇精读正是围绕这一观点展开并结合本仓库中 《AsyncAwait 优越之处》、《Javascript 事件循环与异步》 等姊妹篇从语法糖本质、并发语义、事件循环三个层面把它讲透。2 概述随处可见的顺序 await代码下面是随处可见的现代化前端代码——下单点餐场景每个操作都标注了同步/异步类型(async () { const pizzaData await getPizzaData(); // async call const drinkData await getDrinkData(); // async call const chosenPizza choosePizza(); // sync call const chosenDrink chooseDrink(); // sync call await addPizzaToCart(chosenPizza); // async call await addDrinkToCart(chosenDrink); // async call orderItems(); // async call })();这段代码读起来行云流水但性能隐患就藏在第一行与第二行之间await语法本身没有问题问题在于使用者用错了。当pizzaData与drinkData之间没有依赖关系时顺序的await会让getPizzaData与getDrinkData变成串行执行——整体执行时间最多会增加一倍等于getPizzaData的耗时被额外叠加了一次。回到我们一直吐槽的回调地狱虽然回调代码比较丑但至少两行并列的回调代码并不会带来阻塞。语法的简化换来了性能问题而且这个性能问题会直接落到用户体验上是不是值得我们反思一下2.1 正确做法一先发起 Promise再 await 结果正确的思路是先同时发起异步函数再 await 它们的返回值让异步调用在等待期间并行推进(async () { const pizzaPromise selectPizza(); const drinkPromise selectDrink(); await pizzaPromise; await drinkPromise; orderItems(); // async call })();这里的关键差异在于selectPizza()与selectDrink()的调用在await之前就已经执行并返回 Promise两个异步操作随即并行开始后续的两个await只是等待各自的结果就绪。2.2 正确做法二Promise.all 更可读也可以使用Promise.all让并发意图更直白(async () { Promise.all([selectPizza(), selectDrink()]).then(orderItems); // async call })();对比三个版本可以看出顺序await写法最顺眼但并发度最差先发起再等待的写法性能正确Promise.all则在性能正确的基础上把这批操作是并行的这个意图显式写进了代码里。不要随意地 await它很可能让你的代码性能降低。值得注意的是同样的并发陷阱也出现在 《AsyncAwait 优越之处》 的精读中一个模块需要发送 3 个互不依赖的请求后再渲染如果写成async function mount() { const result1 await fetch(a.json); const result2 await fetch(b.json); const result3 await fetch(c.json); render(result1, result2, result3); }3 个请求便是顺序执行的异步优势被完全浪费真正实现并发仍需Promise.all封装一层async function mount() { const result await Promise.all([ fetch(a.json), fetch(b.json), fetch(c.json) ]); render(...result); }可见无依赖的异步任务必须显式并行是 async/await 时代反复踩坑后沉淀下来的通用结论。3 精读为什么 async/await 会被滥用仔细思考 async/await 被滥用的原因笔者认为是它的功能比较反直觉导致的。首先async/await 真的是语法糖功能也仅是让代码写得舒服一些。先不看它的语法或特性仅从语法糖三个字就能看出它一定局限了某些能力。举个例子我们用 html 标签封装了一个组件带来便利性的同时其功能一定是 html 的子集。又比如某个轮子哥觉得某个组件 API 太复杂于是基于它封装了一个语法糖我们多半可以认为这个便捷性是牺牲了部分功能换来的。功能完整度与使用便利度一直是相互博弈的很多框架思想的不同开源版本几乎都是把功能完整度与便利度按照不同比例混合的结果。那么回到 async/await它要解决的问题是回调地狱带来的灾难a(() { b(() { c(); }); });为了减少嵌套结构太多对大脑造成的冲击async/await 决定这么写await a(); await b(); await c();虽然层级上一致了但逻辑上仍然是嵌套关系——b必须等a完成、c必须等b完成。这不是另一个程度上增加了大脑负担吗而且这个转换还是隐形的所以许多时候我们倾向于忽略它于是造成了语法糖的滥用。3.1 理解语法糖await 的隐形串行语义虽然要正确理解 async/await 的真实效果比较反人类但为了清爽的代码结构以及防止写出低性能的代码还是挺有必要认真理解 async/await 带来的改变。首先async/await 只能实现一部分回调支持的功能——也就是仅能方便应对层层嵌套的场景。其他场景就要动一些脑子了。比如下面两对独立的回调a与c之间没有任何依赖a(() { b(); }); c(() { d(); });如果写成下面的方式虽然一定能保证功能一致但变成了最低效的执行方式await a(); await b(); await c(); await d();因为翻译成回调它就变成了a(() { b(() { c(() { d(); }); }); });然而我们发现原始代码中函数c可以与a同时执行但 async/await 语法会让我们倾向于在b执行完后再执行c——这正是隐形串行化的根源表面上await让代码变平了实际上它把原本可并行的独立任务排成了一条依赖链。所以当我们意识到这一点可以优化一下性能const resA a(); const resC c(); await resA; b(); await resC; d();先同时发起a()与c()再分别等待。但这个逻辑其实仍然无法完全达到回调的效果虽然a与c同时执行了但d原本只需要等待c执行完现在如果a的执行时间比c长d就变成了在等a完成之后才执行等效于a(() { d(); });也就是d被a的慢速执行拖累了并发收益被部分抵消。看来只有完全隔离成两个函数才能恢复原始的并发结构(async () { await a(); b(); })(); (async () { await c(); d(); })();或者利用Promise.all把两条独立链路组织起来async function ab() { await a(); b(); } async function cd() { await c(); d(); } Promise.all([ab(), cd()]);这就是 async/await 的可怕之处。回调方式这么简单的过程式代码换成 async/await 居然写完还要反思一下再反推着去优化性能这简直比回调地狱还要可怕。而且大部分场景的代码是非常复杂的同步与await混杂在一起想捋清楚其中的脉络、并正确优化性能往往是很困难的。但是我们为什么要自己挖坑再填坑呢很多时候还会导致忘了填。原文作者给出了Promise.all的方式简化逻辑但笔者持保留态度不要一味追求 async/await 语法在必要情况下适当使用回调是可以增加代码可读性的。3.2 源码视角async/await 到底做了什么从 《AsyncAwait 优越之处》 的源码级分析可以看到async/await 与 Promise 存在千丝万缕的联系——一个 async 函数总是会返回一个 Promise。因此可以说async/await 只是 Promise 的语法糖这解释了它为什么天生只能表达 Promise 的表达能力。下面是最基础的 async/await 例子async function test() { const img await fetch(tiger.jpg); }使用 Babel 转换后会被改写为基于_asyncToGenerator与regeneratorRuntime的状态机代码use strict; var test function() { var _ref _asyncToGenerator(regeneratorRuntime.mark(function _callee() { var img; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev _context.next) { case 0: _context.next 2; return fetch(tiger.jpg); case 2: img _context.sent; case 3: case end: return _context.stop(); } } }, _callee, this); })); return function test() { return _ref.apply(this, arguments); }; }(); function _asyncToGenerator(fn) { return function() { var gen fn.apply(this, arguments); return new Promise(function(resolve, reject) { function step(key, arg) { try { var info genkey; var value info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function(value) { step(next, value); }, function(err) { step(throw, err); }); } } return step(next); }); }; }从这段转换代码可以推断await在底层就是Promise.resolve(value).then(...)的逐级衔接step(next, value)每推进一次就代表一个await挂起并等待一个 Promise 完成——顺序await的串行化并非引擎优化失误而是这种状态机推进方式最直接的表达。原来只需 3 行代码解决的问题被转换成 52 行代码这还是基于执行环境中已经存在 regenerator 的前提若要在兼容性不佳的 Web 环境下使用代码 overhead 成本也必须纳入考虑。此外async 函数默认返回 Promise 还带来一个隐藏问题异常会被静默吞掉。虽然 async/await 能用try...catch...这种符合同步习惯的方式捕获异常但你依然不得不手动给每个await调用添加try...catch...语句否则async 函数返回的只是一个 reject 掉的 Promise 而已——这与本篇文章讨论的双刃剑主题互为镜像便利的另一面总是隐藏着需要开发者主动补齐的责任。3.3 事件循环视角await 在何时恢复执行要真正理解await为什么不阻塞、以及它的执行时机可以结合 《Javascript 事件循环与异步》 中的原理JS 主线程的同步代码全部运行在调用栈Call Stack中异步代码则通过事件循环Event Loop调度。Promise之流的任务属于 Microtask插入当前执行队列的横排是最快被执行到的一类setTimeout之流的 Macrotask 则被添加到新的纵排执行优先级更低。await恢复执行的本质就是 async 函数内部状态机在对应 Promise 落定settle后通过 Microtask 继续推进step(next, ...)。这也意味着await并不会让出线程它只是把函数的剩余部分挂起等 Promise 就绪后再以 Microtask 的优先级快速恢复。理解这一点才能解释为什么先发起、后等待的写法能并发——因为await挂起的是函数的执行流而不是异步操作本身异步操作从函数调用那一刻就已开始。4 实战准则何时用 async/await何时显式并发综合以上分析可以沉淀出几条可执行的选型准则场景推荐写法原因严格依赖链如取到 id 再查详情顺序await串行是业务语义本身的要求顺序await最直白互不依赖的多个异步任务先发起、后await或Promise.all恢复并发避免执行时间叠加依赖链 独立链路混杂的复杂流程拆分为多个 async 函数用Promise.all组织保持每段链路内串行、链路之间并行可读性与性能兼得复杂流程控制中断、进度、暂停等适当回归回调或引入流程控制库async/await 缺少 abort、progress、pause、resume 等控制能力硬写会自己挖坑再填坑其中严格依赖链的典型例子可见 《AsyncAwait 优越之处》 中的建文档逻辑先db.post({})得到新文档 id再db.get(response.id)按 id 查询——两个操作必须串行此时顺序await就是最优解async function createNewDoc() { let response await db.post({}); // post a new doc return await db.get(response.id); // find by id }而串行本身也有语法层面的选择。在需要一个接一个执行 Promise 队列而非并发时《用 Reduce 实现 Promise 串行执行》 给出了两种等价思路一是用reduce在内存中构造 Promise 队列同步构造、异步执行一个事件循环内完成队列搭建function runPromiseByQueue(myPromises) { myPromises.reduce( (previousPromise, nextPromise) previousPromise.then(() nextPromise()), Promise.resolve() ); }二是在 async/await 支持下简化为for...of逐项等待async function runPromiseByQueue(myPromises) { for (let value of myPromises) { await value(); } }需要注意两种思路的差异reduce版本整体是同步函数先执行完毕构造出 Promise 队列再在内存异步执行for...of版本则是把自身改造成异步函数逐个等待每个 Promise 执行完毕。串行队列在真实业务中往往用得不多因为串行会阻塞而用户交互往往是并行的——这也再次印证了本篇的核心观点并发场景下串行await是最大的性能隐患。5 总结决定代码质量的是思维async/await 双刃剑的讨论提醒着我们不要过度依赖新特性否则可能带来代码执行效率的下降进而影响到用户体验。同时也不要过度利用新特性去修复新特性带来的问题这样反而会导致代码可读性下降。翻开 redux 刚火起来那段时期的老代码可以看到许多过度抽象、为了用而用的代码硬是把两行代码能写完的逻辑拆到了 3 个文件、分散在 6 行不同位置只能靠字符串搜索的方式查找线索最后发现这个抽象整个项目仅用了一次。写出这种代码的可能性只有一个——在精神麻木的情况下一口气喝完了 redux 提供的全部鸡汤。就像 async/await 地狱一样看到这种 redux 代码远不如所谓没跟上时代的老前端写出的 jquery 代码。决定代码质量的是思维而非框架或语法。async/await 虽好但也要适度。补充说明经过讨论笔者把原文的async/await 地狱标题改成了async/await 是把双刃剑。因为 async/await 并没有回调地狱那么可怕称它为地狱有误导的可能性——它牺牲的不是代码的可用性而是并发性能与心智负担二者危害程度并不相同。6 延伸阅读精读《AsyncAwait 优越之处》async/await 的六大优势、Babel 编译产物与并发局限的正面论证精读《Javascript 事件循环与异步》Microtask 与 Macrotask 的调度机制理解await的恢复时机精读《用 Reduce 实现 Promise 串行执行》Promise 队列串行的两种实现及其差异readme.md前端精读周刊的完整目录与全部精读主题索引赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐Async/Await 的优越之处与实现原理前端精读周刊 we/weekly 异步编程详解Async/Await 的优越之处与实现原理前端精读周刊 we/weekly 异步编程详解 本文基于前端精读周刊 readme.md https://lin文档技术博客教程如何利用Supabase-kt在iOS平台构建跨平台应用SwiftUI与Kotlin的完美融合如何利用Supabase kt在iOS平台构建跨平台应用SwiftUI与Kotlin的完美融合 Supabase kt是一个强大的Kotlin多平台客户端库30 分钟搭好一个 ESP32 温湿度光照监测节点硬件不到 150 元30 分钟搭好一个 ESP32 温湿度光照监测节点硬件不到 150 元 昨晚回家卧室又闷又干空调到底有没有在制热、绿植盆土干了没有全靠感觉。这类人不在嵌入式物联网驱动开发创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

一行命令装好 ComfyUI?AMD 8G 显卡保姆级实操

一行命令装好 ComfyUI?AMD 8G 显卡保姆级实操

你是不是已经把显卡驱动更新到最新了,路径也改成纯英文了,但打开 ComfyUI 安装教程一看,全是 WSL2、Docker、conda,头都大了? 别慌,我也踩过这些坑——AMD 装 ComfyUI 确实比 NVIDIA 多几步,但路…

📅 2026/10/3 7:16:46
tlb use_temporary_mm

tlb use_temporary_mm

use_temporary_mm 是 x86 架构中一个用于临时切换到一个专用地址空间(MM)执行敏感操作的函数。它主要服务于内核的文本补丁(text patching) 机制,在修改内核代码时提供一层额外的内存保护。核心目的:安全地…

📅 2026/10/3 7:16:46
USB-C 一线连偶发断连排查:从 PD 供电协商到 USB 重枚举

USB-C 一线连偶发断连排查:从 PD 供电协商到 USB 重枚举

0. 现象 大屏闪一下恢复、摄像头会中掉线、无线键鼠偶发卡顿,重插即好。会自愈,所以进不了工单——但会一直消耗用户的信任。 1. 根因框架:一条线,两份合同 一根全功能 Type-C 同时跑三件事,且分别协商:通道…

📅 2026/10/3 7:16:46
MORE NEWS

更多资讯

📰

ESP32芯片与模组怎么选?从裸芯片到量产料号的选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

DRV8818+TM4C129高精度步进电机控制实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

YOLO Windows训练报错修复:PermissionError与OMP Error #15排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

西南交大数据库原理作业:高级数据模型ER图与主键设计避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

RK3576 SD卡识别失败:硬件信号完整性与内核MMC全链路排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

6.2 EMC 深度解析:测试项目、RJ45防护与整改实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬