async/await:Generator 语法糖的完整脱糖形态
async/await 不是魔法,它是 Generator + Promise 的语法封装——理解这个等价关系,执行时序、错误传播、微任务调度中的所有"奇怪行为"都有了清晰的解释。
🔹 Level 1 · 你需要知道的
async 函数的基本行为
async 函数无论如何都返回 Promise:
async function getValue() {
return 42 // 等价于 return Promise.resolve(42)
}
async function getError() {
throw new Error('fail') // 等价于 return Promise.reject(new Error('fail'))
}
async function getPromise() {
return Promise.resolve(99) // 仍然是 Promise,不是嵌套的 Promise
}
getValue().then(v => console.log(v)) // 42
getError().catch(e => console.log(e.message)) // 'fail'
getPromise().then(v => console.log(v)) // 99
await 的等待语义
await expr 暂停 async 函数,等待 expr 对应的 Promise 完成,然后恢复:
async function fetchUser(id) {
// 暂停,等待 fetch 完成
const response = await fetch(`/api/users/${id}`)
// 暂停,等待 JSON 解析完成
const user = await response.json()
return user
}
// 错误处理
async function safeGet(url) {
try {
const res = await fetch(url)
return await res.json()
} catch (err) {
console.error('请求失败:', err.message)
return null
}
}
串行 vs 并行
await 是串行的,一个接一个;Promise.all 实现并行:
// 串行:总耗时 ≈ a时间 + b时间
async function serial() {
const a = await fetchA() // 等 A 完成
const b = await fetchB() // 等 B 完成
return [a, b]
}
// 并行:总耗时 ≈ max(a时间, b时间)
async function parallel() {
const [a, b] = await Promise.all([fetchA(), fetchB()]) // 同时等
return [a, b]
}
// 常见错误:以为是并行,实际是串行
async function wrongParallel() {
const promiseA = fetchA() // 发起请求(不阻塞)
const promiseB = fetchB() // 发起请求(不阻塞)
const a = await promiseA // 等 A
const b = await promiseB // 等 B
return [a, b]
// 这其实近似并行,因为两个请求都已发出
// 但如果 A 报错,B 的错误会被"吞掉"(unhandled rejection)
// 正确做法仍然是 Promise.all
}
顶层 await(ESM 模块)
在 ES 模块中,可以在模块顶层使用 await(不需要包裹在 async 函数中):
// config.mjs
const config = await fetch('/api/config').then(r => r.json())
export { config }
// 使用 config.mjs 的模块会等待它完成加载
// import { config } from './config.mjs' — 自动等待顶层 await 完成
🔸 Level 2 · 它是怎么运行的
async/await 的脱糖等价形态
理解 async/await 最直接的方式是写出等价的 Generator + Promise 代码:
// === 原始 async/await 代码 ===
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
const user = await response.json()
return user
}
// === 等价的 Generator + Promise 实现(概念上) ===
function fetchUser(id) {
// async 函数返回一个 Promise
return new Promise((resolve, reject) => {
// async 函数体对应一个生成器
const gen = (function* () {
try {
const response = yield fetch(`/api/users/${id}`)
// yield 等价于 await:暂停,向外交出 Promise
const user = yield response.json()
// return 等价于 resolve
resolve(user)
} catch (e) {
// 未捕获的错误等价于 reject
reject(e)
}
})()
// 驱动生成器的函数(相当于 async 运行时)
function step(value) {
let result
try {
result = gen.next(value) // 把上次 await 的结果传回去
} catch (e) {
reject(e)
return
}
if (result.done) return // 生成器结束,Promise 已 resolve
// result.value 是 yield 出来的 Promise
Promise.resolve(result.value).then(
v => step(v), // Promise resolve → 继续执行
e => {
try {
result = gen.throw(e) // Promise reject → 在 yield 处抛出
} catch (e2) {
reject(e2)
}
}
)
}
step(undefined) // 第一次调用,无传入值
})
}
这个等价形态揭示了以下关键点:
- async 函数的每一个
await对应生成器的一个yield await等待的 Promise resolve/reject 通过gen.next(value)/gen.throw(error)传回函数体- 错误传播是通过
gen.throw()注入的,所以try/catch可以捕获await失败
await 的微任务调度模型
遇到 await expr 时,引擎做了什么:
await expr 的执行过程:
async 函数体执行到 await expr
│
▼
1. 对 expr 求值,得到一个值 V
│
▼
2. 若 V 不是 Promise:V = Promise.resolve(V)
(即使是非 Promise,也会产生一次微任务延迟)
│
▼
3. 将"当 V resolve 后恢复 async 函数"注册为 V 的 then 回调
(通过 PerformPromiseThen(V, resumeCallback, rejectCallback))
│
▼
4. async 函数挂起,将控制权交还给调用者
│
▼
5. (当前同步代码继续执行...)
│
▼
6. 当 V resolve 时:
在微任务队列中调度 resumeCallback(resolvedValue)
│
▼
7. 微任务执行:async 函数从 await 处恢复,
await expr 的值为 resolvedValue
console.log('1: 同步开始')
async function demo() {
console.log('3: async 函数开始')
const v = await Promise.resolve('resolved')
console.log('5: await 之后,v =', v)
}
demo()
console.log('4: demo() 调用之后的同步代码')
// 输出顺序:
// 1: 同步开始
// 3: async 函数开始
// 4: demo() 调用之后的同步代码
// 5: await 之后,v = resolved
async 函数的 Promise 何时 resolve
async 函数的 Promise resolve 时机:
async function f() {
// ... 函数体
}
情况 1:函数体 return value
→ f() 返回的 Promise 以 value resolve
情况 2:函数体 throw error
→ f() 返回的 Promise 以 error reject
情况 3:函数体到达末尾(没有 return)
→ f() 返回的 Promise 以 undefined resolve
情况 4:函数体 return aPromise(返回另一个 Promise)
→ f() 返回的 Promise 跟随 aPromise(thenables 会被展平)
→ 不是嵌套的 Promise<Promise<T>>,而是等待 aPromise 完成后 resolve
注意:情况 4 意味着
async function f() { return Promise.reject(1) }
等价于
async function f() { throw 1 }
两者的 f() 返回的 Promise 都会 reject(1)
await 的"双微任务"历史问题
规范曾经要求 await 产生两次微任务调度(当 await 的对象是原生 Promise 时)。ES2020 通过修改规范将其优化为一次:
旧规范(ES2018 之前的行为):
await nativePromise
1. nativePromise.then(v => resolve(tempPromise, v)) ← 第一次微任务
2. tempPromise.then(resumeCallback) ← 第二次微任务
总计:2次微任务延迟
新规范(ES2020 优化后):
await nativePromise
1. nativePromise.then(resumeCallback) ← 直接注册
总计:1次微任务延迟
影响(以下代码在新旧规范下输出顺序不同):
async function f() {
await Promise.resolve() // 旧规范需要2次微任务
console.log('A')
}
f()
Promise.resolve().then(() => console.log('B'))
// 旧规范:B A(B 在第1次微任务,A 在第2次微任务)
// 新规范:A B(await 只需1次微任务)
顶层 await 的模块执行顺序
顶层 await 会暂停当前模块的执行,但不会阻塞兄弟模块(同一父模块的其他依赖):
模块依赖图(顶层 await 场景):
main.mjs
├── a.mjs(有顶层 await,需要500ms)
└── b.mjs(无顶层 await,同步)
加载顺序:
1. 加载 a.mjs → 开始执行 → 遇到顶层 await → 挂起
2. 加载 b.mjs → 同步执行完毕
3. a.mjs 的 await 完成 → a.mjs 继续执行完毕
4. main.mjs 继续执行
规则:
- 顶层 await 阻塞依赖它的模块(main.mjs 等待 a.mjs)
- 顶层 await 不阻塞兄弟模块(b.mjs 正常执行)
- 若 a.mjs 和 b.mjs 都有顶层 await,它们的 await 并发执行
🔺 Level 3 · 规范怎么定义的
27.7 AsyncFunction Objects
规范第 27.7 节定义了 AsyncFunction 对象。调用 async function f() {} 时,f 是一个普通的 ECMAScript 函数对象,只是其 [[ECMAScriptCode]] 在被求值时,引擎会使用 AsyncFunctionStart 来启动异步执行。
AsyncFunctionStart 算法
当 async 函数被调用(F.[[Call]])时,在执行函数体之前,规范执行 AsyncFunctionStart(promiseCapability, asyncFunctionBody):
AsyncFunctionStart(promiseCapability, asyncFunctionBody):
1. 令 runningContext = 运行中的执行上下文
2. 令 asyncContext = runningContext 的副本(新的执行上下文)
3. 设置 asyncContext 的代码求值状态:
当恢复执行时,执行以下步骤:
a. 令 result = asyncFunctionBody 的执行结果
b. 断言:当前执行上下文是 asyncContext(被恢复了)
c. 从执行上下文栈移除 asyncContext
d. 若 result.[[Type]] 是 normal:
调用 promiseCapability.[[Resolve]](result.[[Value]])
否则(return / throw):
若 result.[[Type]] 是 return:
调用 promiseCapability.[[Resolve]](result.[[Value]])
否则:
调用 promiseCapability.[[Reject]](result.[[Value]])
4. 将 asyncContext 推入执行上下文栈
5. 恢复 asyncContext 的执行(即开始执行函数体)
6. 断言:asyncContext 已经从执行上下文栈中移除
(执行到第一个 await 时,asyncContext 被挂起并从栈中移除)
7. 返回 promiseCapability.[[Promise]]
Await 抽象操作
规范中 await expr 对应的操作是 Await(value)(规范 27.7.5.3):
Await(value):
1. 令 asyncContext = 运行中的执行上下文
// 保存当前 async 函数的执行上下文
2. 令 promise = PromiseResolve(%Promise%, value)
// 将 value 包装为 Promise(若已是 native Promise 则直接使用)
3. 令 stepsFulfilled = the algorithm steps defined in Await Fulfilled Functions
4. 令 onFulfilled = CreateBuiltinFunction(stepsFulfilled)
// onFulfilled 是当 promise resolve 时调用的函数:
// 恢复 asyncContext,将 resolvedValue 作为 Await 表达式的值
5. 令 stepsRejected = the algorithm steps defined in Await Rejected Functions
6. 令 onRejected = CreateBuiltinFunction(stepsRejected)
// onRejected 是当 promise reject 时调用的函数:
// 恢复 asyncContext,将 rejectedValue 作为在 await 处抛出的异常
7. PerformPromiseThen(promise, onFulfilled, onRejected)
// 注册回调
8. 从执行上下文栈移除 asyncContext
// 挂起当前 async 函数
9. 将 asyncContext.[[Suspended]] 置为 true
10. 返回(将控制权交还给调用 Await 之前的上下文)
onFulfilled(Await Fulfilled Function)的算法:
Await Fulfilled Function(value):
// 当 await 的 promise resolve 时被调用
1. 令 asyncContext = 这个函数关联的 asyncContext
2. 令 prevContext = 运行中的执行上下文
3. 将 asyncContext 推入执行上下文栈(恢复 async 函数的执行上下文)
4. 在 asyncContext 中:
将 value 作为 Await 操作的 NormalCompletion 值返回
// 即:await 表达式的值是 value
5. 将 asyncContext 从执行上下文栈移除
恢复 prevContext
PerformPromiseThen 在 await 中的调用
规范 27.2.5.4.1 PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability):
PerformPromiseThen(promise, onFulfilled, onRejected):
1. 若 promise.[[PromiseState]] 是 'pending':
- 将 {[[Capability]]: resultCapability, [[Type]]: 'Fulfill', [[Handler]]: onFulfilled}
添加到 promise.[[PromiseFulfillReactions]]
- 将 {[[Capability]]: resultCapability, [[Type]]: 'Reject', [[Handler]]: onRejected}
添加到 promise.[[PromiseRejectReactions]]
2. 若 promise.[[PromiseState]] 是 'fulfilled':
- 令 value = promise.[[PromiseResult]]
- 将 fulfill reaction 加入微任务队列(EnqueueJob)
// 注意:即使 promise 已经 resolve,也是异步(微任务)执行
3. 若 promise.[[PromiseState]] 是 'rejected':
- 类似步骤 2,调度 reject reaction
这解释了为什么 `await 42` 仍然有微任务延迟:
PromiseResolve(42) → 创建一个已 fulfilled 的 Promise
PerformPromiseThen 将 onFulfilled 加入微任务队列
当前同步代码执行完毕后,微任务队列中的 onFulfilled 才执行
→ async 函数在微任务队列中恢复
💎 Level 4 · 边界与陷阱
陷阱 1:await 非 Promise 值仍然有一次微任务延迟
await 42 不是立即继续,它等价于 await Promise.resolve(42),会产生一次微任务调度:
async function f() {
console.log('A')
await 42 // 即使是同步值,也会产生微任务延迟
console.log('C')
}
f()
console.log('B')
// 输出:A → B → C
// 'B' 在 'C' 之前,因为 await 42 产生了微任务延迟
这是规范保证的行为:PromiseResolve(42) 创建一个已 fulfilled 的 Promise,然后 PerformPromiseThen 将恢复回调加入微任务队列,而不是同步执行。
陷阱 2:return Promise.reject(1) 与 throw 1 的等价性
async function f1() { return Promise.reject(1) }
async function f2() { throw 1 }
// 两者返回的 Promise 都以 1 reject
f1().catch(e => console.log('f1:', e)) // f1: 1
f2().catch(e => console.log('f2:', e)) // f2: 1
// 原因:async 函数遇到 return somePromise 时,
// 不是直接 resolve(somePromise),而是"跟随" somePromise:
// 若 somePromise reject,外层 Promise 也 reject
// 微妙的差异:return 一个 reject 的 Promise 比 throw 多一次微任务延迟
// (因为需要等待 somePromise settle 才能决定外层 Promise 的状态)
陷阱 3:并发请求中的"静默错误"
用 await a; await b 写并发请求时,如果 b 先 reject,它的错误可能变成 unhandled rejection:
// 危险写法:b 先 reject 时,b 的错误可能未被处理
async function dangerous() {
const pa = slowRequest() // 假设需要 1000ms
const pb = fastFail() // 假设 100ms 后 reject
const a = await pa // 等待 pa(1000ms)
// 此时 pb 已经 reject 了 900ms,但我们还没有 await pb
// 在某些环境下,这段时间里 pb 的 rejection 可能被标记为 unhandled
const b = await pb // 这里才会 catch 到 pb 的错误
return [a, b]
}
// 安全写法:Promise.all 确保两个 rejection 都被处理
async function safe() {
const [a, b] = await Promise.all([slowRequest(), fastFail()])
return [a, b]
}
// 若需要独立处理每个错误:
async function safeIndependent() {
const [a, b] = await Promise.allSettled([slowRequest(), fastFail()])
if (a.status === 'rejected') handleErrorA(a.reason)
if (b.status === 'rejected') handleErrorB(b.reason)
}
陷阱 4:顶层 await 对模块初始化顺序的影响
顶层 await 会让依赖该模块的所有上层模块等待,可能造成意外的初始化顺序问题:
// database.mjs — 有顶层 await
export const db = await connectToDatabase() // 假设需要 200ms
// logger.mjs — 无顶层 await,同步完成
export const logger = new Logger()
// app.mjs — 依赖两者
import { db } from './database.mjs'
import { logger } from './logger.mjs'
// 执行顺序:
// 1. database.mjs 开始执行 → await connectToDatabase() → 挂起
// 2. logger.mjs 开始执行 → 同步完成
// 3. database.mjs 的 await 完成 → 继续执行
// 4. app.mjs 开始执行(两个依赖都完成后)
// 注意:若 database.mjs 的顶层 await 抛出错误,
// app.mjs 的加载也会失败(但 logger.mjs 不受影响,它已执行完)
陷阱 5:for await...of vs for...of 与异步迭代器
for await...of 使用 Symbol.asyncIterator,而不是 Symbol.iterator:
// 错误:用 for...of 消费异步迭代器
async function* asyncRange(start, end) {
for (let i = start; i < end; i++) {
await delay(100)
yield i
}
}
// 这个写法有问题:for...of 不会 await 每个 yield 的 Promise
// (实际上不会报错,但得到的是 Promise 对象而不是值)
// for (const v of asyncRange(1, 5)) {
// console.log(v) // 打印 Promise,不是数字
// }
// 正确:for await...of
async function main() {
for await (const v of asyncRange(1, 5)) {
console.log(v) // 1, 2, 3, 4(每次等待 100ms)
}
}
// for await...of 内部等价于:
// const iter = asyncRange(1, 5)[Symbol.asyncIterator]()
// while (true) {
// const { value, done } = await iter.next()
// if (done) break
// console.log(value)
// }
本章小结
- async/await 是 Generator + Promise 的语法封装:每个
await对应一个yield,引擎驱动生成器前进,通过gen.next(value)和gen.throw(error)传递 Promise 的 resolve/reject 值。 await expr总是产生至少一次微任务延迟,即使expr是已 resolve 的 Promise 或普通值;这意味着await 42之后的代码总是异步执行,绝不会同步运行。return Promise.reject(e)与throw e在 async 函数中行为等价,但前者多一次微任务延迟(需要等待传入的 Promise settle)。- 串行 await(
await a; await b)总耗时是两者之和;并行应使用Promise.all;混用并发 Promise 但顺序 await 时需警惕 unhandled rejection 问题。 - 顶层 await 阻塞依赖它的父模块,但不阻塞兄弟模块;ES2020 将 await 的微任务延迟从 2 次优化为 1 次,影响依赖精确微任务顺序的代码行为。