第 24 章

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)  // 第一次调用,无传入值
  })
}

这个等价形态揭示了以下关键点:

  1. async 函数的每一个 await 对应生成器的一个 yield
  2. await 等待的 Promise resolve/reject 通过 gen.next(value) / gen.throw(error) 传回函数体
  3. 错误传播是通过 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)
// }

本章小结

  1. async/await 是 Generator + Promise 的语法封装:每个 await 对应一个 yield,引擎驱动生成器前进,通过 gen.next(value)gen.throw(error) 传递 Promise 的 resolve/reject 值。
  2. await expr 总是产生至少一次微任务延迟,即使 expr 是已 resolve 的 Promise 或普通值;这意味着 await 42 之后的代码总是异步执行,绝不会同步运行。
  3. return Promise.reject(e)throw e 在 async 函数中行为等价,但前者多一次微任务延迟(需要等待传入的 Promise settle)。
  4. 串行 await(await a; await b)总耗时是两者之和;并行应使用 Promise.all;混用并发 Promise 但顺序 await 时需警惕 unhandled rejection 问题。
  5. 顶层 await 阻塞依赖它的父模块,但不阻塞兄弟模块;ES2020 将 await 的微任务延迟从 2 次优化为 1 次,影响依赖精确微任务顺序的代码行为。
本章评分
4.6  / 5  (6 评分)

💬 留言讨论