Async / Await:用同步的方式写异步,真香!

Async / Await:用同步的方式写异步,真香!
Toukoasync/await 是 ES2017 引入的强大特性,使得处理异步操作变得更加简单和直观。使用 async 声明的函数总是返回一个 Promise,而 await 关键字则像是它的指挥棒,允许你暂停函数的执行,直到 Promise 被解决或拒绝。这种方式让异步代码看起来更像同步代码,从而提高了可读性和可维护性。尤其在处理链式异步操作和错误捕获时,async/await 显示出其独特的优势。
前置知识
如果对生成器函数和 yield 不太熟悉,可以先看看下面这篇,了解基础后再看 Async / Await 会更顺~
一、Async / Await 的本质:老熟人的默契配合
你可能觉得 Async / Await 是 JavaScript 里的 “新黑科技”,但其实它的底层是三个老熟人在搭班子干活。说穿了,就是把咱们早就眼熟的技术组合得更顺手了。
1.1 三大核心组件
- Generator(生成器):提供暂停 - 恢复的能力,就像给函数装了个暂停键
- Promise:处理异步操作的 “标准接口”,负责管理异步结果
- 自动执行器:默默工作的调度员,悄悄驱动生成器跑完全程
1.2 从代码看转换逻辑
咱们写的 Async 函数,其实会被引擎偷偷转换成类似生成器的结构。比如这样一段代码:
1 | // 咱们写的优雅代码 |
引擎背地里会把它转成差不多这样(简化版):
1 | // 引擎实际处理的样子 |
这里有个小细节: 每个 Async 函数都被转换成一个生成器函数,由自动执行器接管执行!
咱们不用手动调用next(),全是执行器在后台搞定,这也是Async/Await比直接用Generator方便的地方~二、自动执行器:幕后的引擎
自动执行器是 Async / Await 能自动跑起来的关键,我试着简化了它的核心代码,大概长这样:
2.1 核心逻辑(简化版)
1 | function spawn(generatorFunc) { |
2.2 执行过程图解
我画了个流程图帮大家理解,其实就是执行器在中间当 “裁判”,协调生成器和异步操作:
sequenceDiagram
participant 调用者
participant Async 函数
participant 执行器
participant 生成器
调用者->>Async函数: 调用 asyncFunc()
Async函数->>执行器: “麻烦帮我跑一下这个生成器”
执行器->>生成器: “开始执行咯(调用next())”
生成器-->>执行器: “遇到await了,先停在这”(返回yield的值)
执行器->>Promise: “等你结果出来喊我”
Promise-->>执行器: “搞定,结果在这”
执行器->>生成器: “继续跑吧,这是刚才的结果”(调用next(结果))
生成器-->>执行器: “跑完了,这是最终结果”
执行器->>调用者: “任务完成,给你结果”(Promise resolved)
简单说就是:执行器启动生成器后,每次遇到 yield(也就是咱们写的 await)就停下来等异步结果,拿到结果再叫醒生成器继续跑,直到结束。全程不用手动干预,比直接用 Generator 省太多事了~
三、await 到底做了什么?四步看懂它的 “小动作”
每次写await的时候,引擎其实在背后干了四件事,我拆开来给大家说说:
- 暂停当前函数:就像按了暂停键,当前的变量、执行位置都被 “冻” 起来
- 包装异步结果:不管 await 后面是 Promise 还是普通值(比如 await 42),都会被转成 Promise。普通值会被 Promise.resolve() 包一层,确保统一用 Promise 处理
- 注册回调:把 await 后面的代码(比如拿到 data 后处理的逻辑)打包成一个微任务,注册到事件循环里
- 让出主线程:当前函数暂停后,主线程会去执行其他任务(比如渲染、处理其他事件),等 Promise 有结果了,再回头执行刚才打包的微任务
这里有个性能小细节: await不会阻塞主线程!它只是把后续代码挂起(包装成微任务),让主线程先忙别的。这也是为什么用await的时候,页面不会卡 —— 因为它会主动 “让道”。
四、错误处理:try / catch 居然能管到异步操作?
这是我觉得 Async/Await 最方便的一点:用同步代码里的 try/catch 就能搞定异步错误,不用像回调那样嵌套多层 error 处理。
4.1 同步式的错误处理
1 | async function fetchUser() { |
4.2 背后的错误传递
为什么 try / catch 能抓到异步错误?秘密在自动执行器的这段代码里:
1 | // 自动执行器处理Promise的部分 |
当 await 后面的 Promise 失败时,执行器会调用generator.throw(e),把错误 “扔回” 生成器函数内部。这时候生成器里的 try / catch 就会像捕获同步错误一样,把这个异步错误接住。
之前用回调的时候,每次异步操作都要单独写 error 处理,现在一个 try / catch 全搞定,代码清爽多了~
五、性能优化:我在项目里掉过的性能坑
分享几个我实际开发中遇到的问题,都是关于 Async / Await 性能的,新手很容易踩坑:
5.1 坑一:没必要的顺序执行
1 | // 反面例子:两个请求本来可以同时跑,却写成了顺序执行 |
这两个请求如果没依赖关系(比如 A 不影响 B 的参数),完全可以同时启动,我后来改成这样:
1 | // 优化后:同时启动两个请求,总耗时是最慢那个的时间 |
5.2 坑二:多余的 await 包装
有时候会下意识地在 return 前面加 await,但其实没必要:
1 | // 多余的await |
因为 Async 函数会自动把返回值包成 Promise,这里的 await 纯属多此一举,直接 return 就行:
1 | // 更简洁高效 |
5.3 多个异步操作:用 Promise.all() 处理并行
如果需要等多个异步操作都完成,Promise.all()配合 await 是绝配,下面的写法在实际项目中会经常用到哦~
1 | async function fetchAll() { |
不过要注意: Promise.all()是 “一损俱损”,只要有一个请求失败,整个就会报错,这时候可以用Promise.allSettled()处理需要全部结果的场景(哪怕部分失败)。
5.4 高级技巧:模块里的顶级 await
现在很多打包工具(比如 Webpack、Vite)已经支持模块顶层的 await 了,不用再包在 Async 函数里:
1 | // 直接在模块顶层用await |
我在做一个工具库的时候用过这个,用来加载动态配置,比以前用 IIFE(立即执行函数)清爽多了。
六、Async / Await 实战场景:这些地方用起来超顺手
6.1 异步初始化(比如数据库连接)
1 | class Database { |
这种场景如果用回调,很容易写成嵌套的 init 回调,用 Async / Await 就清晰多了。
6.2 有依赖关系的顺序请求
比如先拿用户 ID,再用 ID 查订单:
1 | async function purchase(itemId) { |
步骤再复杂,用顺序 await 写出来也像同步代码一样好懂。
6.3 带重试的请求
处理可能偶尔失败的接口时,用 Async / Await 写重试逻辑很直观:
1 | async function fetchWithRetry(url, retries = 3) { |
我在对接一个不稳定的第三方接口时,就用这个逻辑做了重试,成功率提高了不少。
6.4 超时控制(防止请求卡太久)
结合Promise.race()实现超时控制:
1 | async function fetchWithTimeout(url, timeout = 5000) { |
这个在做支付回调的时候特别有用,防止因为网络问题让用户一直等。
七、踩过的坑:这些细节要注意
7.1 箭头函数的 this 陷阱
用 Async 箭头函数当对象方法时,this会丢:
1 | // 有问题:this指向不对 |
我在写一个类的方法时犯过这个错,调试了半天才发现是箭头函数的锅。
7.2 控制并发数量
如果并行请求太多(比如一次发 20 个接口),可能会触发浏览器的并发限制,这时候需要控制并发数:
1 | async function processBatch(items) { |
这里的throttlePromises是一个工具函数,原理是把任务分成多批,一批批执行(每批 5 个),避免一次性发起太多请求。
7.3 可取消的异步任务(结合 AbortSignal)
有时候需要中途取消异步操作(比如用户离开页面),可以用AbortSignal:
1 | async function longRunningTask(abortSignal) { |
我在做一个文件上传组件时用过这个,用户点取消按钮时,就通过AbortSignal终止上传。
八、常见问题解答(我当初学的时候也纠结过)
Q1: Async 函数会阻塞主线程吗?
不会! Async 函数遇到 await 时会暂停并释放主线程,JavaScript 的单线程模型通过事件循环实现异步执行。
1 | async function test() { |
执行到 await 时,函数会暂停,主线程可以去处理其他任务(比如点击事件、渲染),等 1 秒后才回头执行 console.log(‘结束’),所以不会卡页面。
Q2: 可以 await 一个非 Promise 值吗?
可以! 引擎会自动用 Promise.resolve() 包装非 Promise 值:
1 | async function getNumber() { |
不过实际开发中很少这么用,一般 await 后面都是异步操作返回的 Promise。
Q3: 为什么 Async 函数不管 return 什么,最后都返回 Promise?
因为它本质是异步操作的包装器。哪怕你 return 一个原始值,引擎也会用 Promise.resolve() 包一层:
1 | async function answer() { |
所以调用 Async 函数时,必须用await或者.then()才能拿到结果。
Q4: 用 Promise.all() 的时候,如果有一个请求失败怎么办?
之前已经提到过了,Promise.all() 会 “快速失败”—— 只要有一个 Promise 被拒绝,整个 Promise.all() 就会立刻失败,进入 catch。如果需要等所有请求完成(不管成功失败),可以用Promise.allSettled():
1 | async function fetchAll() { |
Q5: 为什么说 Async / Await 比回调好?
我总结了几个实际开发中的感受:
- 代码不嵌套,扁平结构更易读(告别 “回调地狱”)
- 错误处理统一用 try / catch,不用每层回调都写 error 处理
- 逻辑顺序和代码执行顺序一致,不用跳来跳去看代码
- 调试更方便,错误堆栈更完整(回调的堆栈经常被异步操作打断)
其实 Async/Await 不算什么高深的技术,就是把 Generator、Promise 这些老东西包装得更好用了。但正是这种 “语法糖”,让我们写异步代码时能少掉很多头发~ 如果你也有过用 Async/Await 踩坑的经历,欢迎在评论区分享呀!











