前言

async/await 是当前 TypeScript/JavaScript 中处理异步操作的主要方式。它的流行极大解决了过去前端异步编程的各种痛点,让异步编程大步跨入现代化时代。

本文将深入探讨 async/await 的各个层面,尽力描绘清楚 TypeScript/JavaScript 中 async/await 的真实面貌。


一、什么是 async / await

async/await 是 ES2017 引入的语法,用于简化 Promise 的异步操作,本质上是一种无栈协程的实现。

先看个例子:

async function fetchData() {
try {
const response = await fetch('/api/user')
const user = await response.json()
const postsResponse = await fetch(`/api/posts/${user.id}`)
const posts = await postsResponse.json()
console.log(posts)
} catch (error) {
console.error(error)
}
}

获取数据、解析请求结果、将请求结果作为依赖项进行下一步数据请求 —— 这种需要 “等待” 某个事情完成然后再进行下一步的操作,就是使用 async/await 的绝佳场景。代码看起来跟阅读文章一样自然顺畅。

C# 开发者看到这 API 设计应该会无比亲切~

作为对比,使用上一代流行的异步编程技术 Promise 来做同样的事情:

function fetchData() {
return fetch('/api/user')
.then(response => response.json())
.then(user => {
return fetch(`/api/posts/${user.id}`)
})
.then(response => response.json())
.then(posts => {
console.log(posts)
})
.catch(error => {
console.error(error)
})
}

主要差异在几个方面:Promise 需要链式调用,每个操作的结果必须在回调里处理;async/await 可以从上到下线性阅读,像同步代码一样直观。变量作用域也是个问题,Promise 的每个 then 都是新作用域,需要手动传递数据;而 async/await 整个函数共享作用域,变量自然可用。错误处理方面,Promise 要在每个 then 里处理或最后统一 catch,async/await 可以用统一的 try/catch。调试时,Promise 的断点会在多个 then 之间跳转,async/await 则像同步代码一样可以逐步调试。

这个例子相对简单,要真正理解为什么 async/await 能成为主流,需要回顾一下前端异步编程的发展历程。


二、前端异步编程发展历史

回调函数时代

JavaScript 早期,异步操作主要通过回调函数处理。对于有数据依赖的多个异步操作,代码往往会形成层层嵌套:

// 嵌套回调 - 回调地狱
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
// ...层层嵌套
})
})
})
})

这个阶段前端开发者需要面临的痛点主要有这些:

  • 代码层层嵌套,难以阅读
  • 错误处理复杂
  • 控制流不清晰

为了解决这些问题,Promise 应运而生。但在 Promise 成为 ES6 标准之前,有一个重要的过渡阶段。

jQuery 的 Deferred 对象

在 Promise 标准化之前,jQuery 的 Deferred 对象是前端最早广泛使用的 Promise-like 实现。早在 2010 年发布的 jQuery 1.5 中,就引入了这个特性。

当时前端开始摆脱写写特效的阶段,应用逐渐复杂,AJAX 请求大量使用,回调地狱问题日益严重。jQuery 团队设计了一个解决方案:

// jQuery 1.5+ 的 AJAX 请求返回 Promise 对象
$.ajax('/api/user')
.done(function(user) {
console.log('Success:', user)
})
.fail(function(error) {
console.error('Error:', error)
})
.always(function() {
console.log('Complete')
})
// 并行处理多个请求
$.when(getData1(), getData2(), getData3())
.done(function(data1, data2, data3) {
console.log('All done:', data1, data2, data3)
})
.fail(function() {
console.error('One or more failed')
})

这些 API 设计跟标准的 Promise 已经非常相似了。deferred.done() 对应 promise.then()deferred.fail() 对应 promise.catch()deferred.always() 对应后来的 promise.finally()。至于状态控制,deferred.resolve()deferred.reject() 对应 Promise 的 resolve()reject(),并行处理方面 $.when() 对应 Promise.all()

jQuery 的 Deferred 虽然不是最终的标准,但为 Promise 的标准化奠定了重要基础。这个实践让广大前端开发者认识到 Promise 的价值,证明了链式调用确实可以解决回调地狱。更重要的是,它在大量的生产环境中得到了验证,这些实践经验深刻影响了后续 ES6 Promise 的设计。

Promise 标准化 (ES6 / 2015)

2015 年,ES6 (ECMAScript 2015) 正式将 Promise 纳入语言标准:

// 基础用法
const promise = new Promise<string>((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('Success!')
} else {
reject(new Error('Failed!'))
}
}, 1000)
})
promise
.then(result => console.log(result))
.catch(error => console.error(error))
// 链式调用
getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => getMoreData(c))
.catch(error => {
// 统一错误处理
})
// 并行处理
Promise.all([getData1(), getData2(), getData3()])
.then(([data1, data2, data3]) => {
console.log('All done:', data1, data2, data3)
})
.catch(error => {
console.error('One failed:', error)
})

Promise 的改进:

  • 链式调用,代码更扁平,避免回调地狱
  • 统一的错误处理机制,支持错误冒泡
  • 状态机 (pending/fulfilled/rejected) 易于理解和使用
  • 标准化 API,为跨平台一致打下基础

但 Promise 也有局限:

  • 仍然依赖回调,不如同步代码直观
  • 调试时断点跳转不直观
  • 在 then 内部需要返回值来传递数据

就在 Promise 广泛流行的同时,ES6 引入的 Generator 函数为异步编程带来了全新的可能性。

Generator + 执行器 (ES6 / 2015)

Generator 函数提供了一种”可以暂停和恢复”的执行机制。这个特性本意是为了简化迭代器的实现,但开发者们很快发现它可以用来实现异步编程!

Generator 的基本用法

function* simpleGenerator() {
console.log('Step 1')
yield 1
console.log('Step 2')
yield 2
console.log('Step 3')
return 3
}
const gen = simpleGenerator()
console.log(gen.next()) // { value: 1, done: false }, 输出: Step 1
console.log(gen.next()) // { value: 2, done: false }, 输出: Step 2
console.log(gen.next()) // { value: 3, done: true }, 输出: Step 3

Generator 的关键特性:

  • function* 声明生成器函数
  • yield 暂停执行,返回值
  • .next() 恢复执行,可以传值给 yield
  • 函数内部状态会被保存

用 Generator 处理异步操作

开发者们意识到:如果能自动执行 Generator,把 Promise 的结果传回给 yield,就能实现”同步式”的异步代码!

function* fetchUser() {
const response = yield fetch('/api/user')
const user = yield response.json()
const posts = yield fetch(`/api/posts/${user.id}`)
return posts
}
// 需要一个执行器来自动运行
function run(generator) {
const iterator = generator()
function step(result) {
if (result.done) return Promise.resolve(result.value)
return Promise.resolve(result.value)
.then(data => step(iterator.next(data)))
.catch(error => step(iterator.throw(error)))
}
return step(iterator.next())
}
// 使用
run(fetchUser).then(posts => {
console.log(posts)
})

手动写执行器有点麻烦,于是 TJ Holowaychuk(Express.js 的作者)在 2013 年发布了 co 库,把这个执行器逻辑封装好:

const co = require('co')
co(function* () {
const user = yield User.findById(1)
const posts = yield Post.findByUser(user.id)
return { user, posts }
}).then(result => {
console.log(result)
}).catch(err => {
console.error(err)
})

co 会自动执行 Generator,把 Promise 的结果传回给 yield。代码看起来完全同步,但实际执行是异步的。

Node.js 的 Koa 框架

同样的思路,TJ 在 2013 年末创建了 Koa.js —— 一个基于 Generator 的 web 框架。Generator 的”暂停-恢复”特性特别适合实现中间件机制:

const Koa = require('koa')
const app = new Koa()
app.use(function* (next) {
console.log('→ 1')
yield next // 暂停,把控制权交给下一个中间件
console.log('← 1')
})
app.use(function* (next) {
console.log('→ 2')
this.body = 'Hello'
console.log('← 2')
})
// 输出:→ 1 → 2 ← 2 ← 1

下游中间件先执行,yield next 暂停当前中间件,把控制权交给下游。下游执行完后,恢复执行剩余部分。这就是著名的”洋葱模型”。

Babel 时代的 async/await

Generator + co 的方案很好,但需要额外库支持。在 async/await 正式成为 ES 标准之前,Babel 就已经支持将其转译为 Generator + 执行器的代码:

Terminal window
npm install --save-dev babel-plugin-transform-async-to-generator

核心转译步骤:

  1. async 函数包装 - 将 async 函数转换为返回 Promise 的普通函数
  2. Generator 实现 - 函数体被改写为 Generator,await 变成 yield
  3. 自动执行器 - 生成一个递归执行器,自动驱动 Generator 运行
  4. Promise 链接 - 每次 yield 的值都会被包装成 Promise,完成后继续 next

这种转译方式让开发者可以在 2015-2016 年就提前使用 async/await 语法。

在 2013-2016 年间,基于 Generator 的异步编程方案非常流行。cokoaco-bodyco-request 等大量相关库涌现出来,证明了”同步式写法”处理异步的可行性,为 async/await 的出现铺平了道路。

但 Generator 也有局限:需要额外的执行器、不是标准语法、学习曲线较陡、错误栈不够清晰。这些局限最终催生了 async/await —— 一个既保留 Generator 同步式写法优点,又无需额外执行器的标准化方案。

async/await 标准化 (ES2017)

2017 年,async/await 正式成为 ECMAScript 标准。语法几乎和 Generator + co 一模一样,但不需要额外库:

async function fetchUser() {
const response = await fetch('/api/user')
const user = await response.json()
return user
}
// 使用
fetchUser().then(user => {
console.log(user)
})

代码完全同步风格,内置执行器,更好的错误处理(try/catch),标准语法让所有现代浏览器和 Node.js 原生支持。

// Generator + co (2013-2016)
co(function* () {
const user = yield User.findById(1)
const posts = yield Post.findByUser(user.id)
return { user, posts }
})
// async/await (2017+)
async function main() {
const user = await User.findById(1)
const posts = await Post.findByUser(user.id)
return { user, posts }
}

语法几乎一模一样,但 async/await 不需要额外库。

从回调到 Promise,再到 Generator,最终到 async/await,每一步都在解决前一代的痛点。接下来探讨 async/await 的技术本质。


三、协程

文章开头提到,TypeScript/JavaScript 的 async/await 本质上是一种无栈协程实现。那什么是协程?

什么是协程?

协程 (Coroutine) 可以理解成一种”可以暂停和恢复的函数”。

想象你在玩游戏:

  • 普通函数:进入一场无法中断的战斗,一旦开始就必须一口气打完
  • 协程:玩到一半可以存个档,然后退出去做别的事,回头再从存档点继续玩

协程的关键在于”协作”:它不是被操作系统强行切换(像线程那样被抢占),而是主动让出控制权,等时机成熟再恢复执行。

有栈协程 vs 无栈协程

协程有两种实现方式,有栈协程和无栈协程,区别在于如何保存暂停时的”现场”。

有栈协程

每个协程有自己独立的栈空间(就像每个游戏都有独立的存档文件),可以在任意函数调用处暂停(就像可以在游戏中任意位置存档)。

因为每个协程都需要独立栈,内存开销会相对大些。

主流编程语言中,Go、Lua、Java(Virtual Threads)等都选择采用这种方式。

以 Go 为代表,看看例子:

package main
import (
"fmt"
"time"
)
func loadTextures() {
fmt.Println("加载纹理...")
time.Sleep(2 * time.Second)
fmt.Println("纹理完成")
}
func loadSounds() {
fmt.Println("加载音效...")
time.Sleep(1 * time.Second)
fmt.Println("音效完成")
}
func loadMaps() {
fmt.Println("加载地图...")
time.Sleep(1 * time.Second)
fmt.Println("地图完成")
}
func main() {
// 关键点:go 关键字可以在任意函数调用处创建新协程
// 有栈协程可以在任意函数暂停,每个 goroutine 有独立栈
go loadTextures() // 立即返回,并行执行
go loadSounds() // 立即返回,并行执行
go loadMaps() // 立即返回,并行执行
time.Sleep(4 * time.Second) // 等待所有任务完成
fmt.Println("全部完成!")
}

输出:

加载地图...
加载纹理...
加载音效...
音效完成
地图完成
纹理完成
全部完成!

无栈协程

无栈协程的话,选择不维护独立的栈,而是利用编译器把代码转换成状态机,就像游戏引擎用一个状态变量记录”当前执行到哪个关卡、哪个步骤”。

因为不用维护独立栈,所以内存开销会小一些。不过也不全是好事,这种转换相对没那么灵活,中断点只能在特定的位置,例如在每个 await 的地方暂停(就像只能在游戏的存档点存档,不能自由在任意地方存档)。

主流编程语言中,Python (async/await)、C# (async/await)、JavaScript/TypeScript (async/await)、Rust (async/await)、C++20 (co_await) 等都有采用这种协程。

需要说明的是:“无栈”并非指完全没有栈,而是指暂停时物理调用栈被清空,上下文保存到堆上的对象中(如 V8 的 JSGeneratorObject),而非像有栈协程那样维护独立的调用栈。

以 TypeScript 为代表,看看例子:

// 辅助函数
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function loadTextures() {
console.log('加载纹理...')
await delay(2000)
console.log('纹理完成')
}
async function loadSounds() {
console.log('加载音效...')
await delay(1000)
console.log('音效完成')
}
async function loadMaps() {
console.log('加载地图...')
await delay(1000)
console.log('地图完成')
}
async function loadAllResources() {
// 关键点:只能在 await 处暂停,编译器转换为状态机
// 无栈协程没有独立栈,内存开销极小
await Promise.all([
loadTextures(),
loadSounds(),
loadMaps()
])
console.log('全部完成!')
}
loadAllResources()

输出:

加载纹理...
加载音效...
加载地图...
音效完成
地图完成
纹理完成
全部完成!

简单总结

有栈协程像每个游戏都有独立的存档文件,随时可以在任意位置存档;无栈协程像用一个状态变量记录”当前进度”,只能在指定的存档点暂停。后者更轻量,但灵活性稍差。

无栈协程(async/await)在 JavaScript 中是怎么实现的?

V8 原生实现

V8 引擎 (Node.js 和 Chrome 的 JS 引擎) 为例子,async/await 使用内置的原生实现

该实现可以简单概括为:async/await = 状态机 + Promise + 微任务

我们搭配一个例子来讲这个式子。

async function foo() {
// 状态 0:从函数开始到第一个 await
const a = 1;
const result = await bar(); // 暂停点
// 状态 1:从 await 恢复到函数结束
const b = 2;
return a + b + result.length;
}
function bar() {
return Promise.resolve('done')
}
foo().then(res => console.log(res)) // 输出 3

一、状态机对象(上下文容器与现场保存)

async/await 的 “无栈暂停” 能力,由 V8 内部的 JSAsyncFunctionObject 提供(它继承自 JSGeneratorObject,复用了 Generator 的状态机机制)。核心职责是 “保存现场” 和 “恢复现场”。

JSAsyncFunctionObject 包含以下关键字段:

  • function: 指向 async 函数本身。
  • context: 保存闭包上下文。
  • continuation (重点): 这是一个整数(Smi),标记了当前状态机的状态 ID(即字节码偏移量)。
  • parameters_and_registers: 这是一个 FixedArray,用来保存原本在栈上的所有局部变量和参数。
  • promise (async 特有): 保存关联的 Promise 对象。

await 发生时,当前函数帧从调用栈弹出,局部变量保存到堆上的 JSAsyncFunctionObject 中。

这个角色相关的事件有以下:

  1. 编译器的转换:从 async 到 “状态机”

V8 的编译器(Ignition)会把 foo 函数编译为字节码,并按 await 切分为多个状态:

  • 编译时:字节码生成 V8 会为 foo 函数生成”可恢复函数”(Resumable Function)的字节码,关键字节码包括(可通过 --print-bytecode 查看):

    • SwitchOnGeneratorState:状态分发,恢复时根据 continuation 跳转到对应位置
    • InvokeIntrinsic [_AsyncFunctionEnter]:创建 JSAsyncFunctionObject 协程对象
    • InvokeIntrinsic [_AsyncFunctionAwaitUncaught]:处理 await 表达式
    • SuspendGenerator:暂停点标记,保存现场并退出
    • ResumeGenerator:恢复执行,从暂停点继续
  • 运行时:状态机切分

    • 状态 0:从函数开始执行 const a = 1,遇到 await 后暂停
    • 状态 1:从 await 恢复,执行 const b = 2return a + b,函数结束
    • 状态 -1:终止状态(continuation = -1
  1. 状态机对象的核心字段(对应源码)在 src/objects/js-generator.tq 中定义:
// JSGeneratorObject 基类(定义在 src/objects/js-generator.tq)
extern class JSGeneratorObject extends JSObject {
function: JSFunction; // 指向函数本身
context: Context; // 闭包上下文
receiver: JSAny; // this 绑定
input_or_debug_pos: Object; // 输入值或调试信息
resume_mode: Smi; // 恢复模式(kNext/kThrow)
continuation: Smi; // 字节码偏移量(初始为 kGeneratorExecuting),标记恢复时跳转的位置(对应上面例子的状态 0、1)
parameters_and_registers: FixedArray; // 保存局部变量和寄存器
}
// JSAsyncFunctionObject 继承自 JSGeneratorObject
extern class JSAsyncFunctionObject extends JSGeneratorObject {
promise: JSPromise; // 关联的 Promise 对象
await_resolve_closure: JSAny; // 复用优化:resolve 回调闭包
await_reject_closure: JSAny; // 复用优化:reject 回调闭包
}

JSAsyncFunctionObject 继承了 JSGeneratorObject 的所有字段(包括 continuationparameters_and_registers),并额外添加了 promise 字段用于关联异步函数返回的 Promise。

  1. 暂停时做了什么?
  • 当执行到 await bar() 时:
  • 保存数据:把局部变量 a = 1 存入 parameters_and_registers 数组。
  • 保存位置:把 continuation 设为 1(表示下次恢复要从 “状态 1” 开始)。
  • 交出控制权:函数退出,返回一个 Promise

二、Promise(暂停的触发器与结果载体)

await 本质上是在 “等一个 Promise”。JSGeneratorObject 负责 “暂停”,而 Promise 负责 “通知何时恢复”。

  1. async 函数的返回值

任何 async function 的返回值都是一个 Promise。这是 V8 自动包装的: 如果函数正常完成,则 Promise 变为 fulfilled。 如果函数抛出错误,则 Promise 变为 rejected

  1. await 背后的 Promise.then()

当状态机对象暂停并返回 bar()Promise 后,V8 内部的 AsyncFunction 机制会做一件事: 调用 PerformPromiseThen(在 src/builtins/builtins-promise.cc 中,修订:V8 新版本使用 AwaitWithReusableClosures 来优化回调闭包的复用)。 这相当于自动生成了以下逻辑:

// 伪代码:V8 内部自动做的事情
const asyncFuncObj = foo() // 创建状态机对象
const promiseFromAwait = /* 执行到 await,拿到 bar() 的 Promise */
promiseFromAwait.then(
(value) => resume(asyncFuncObj, value), // Promise 成功了,恢复状态机
(error) => throw(asyncFuncObj, error) // Promise 失败了,向状态机抛错
)

关键点:Promise 在这里充当了一个 “信使”。它解决了 “我什么时候该恢复状态机” 的问题。

PerformPromiseThen 的行为取决于 Promise 的状态:

  • 如果 Promise 已经是 fulfilledrejected 状态(如 Promise.resolve('done')),回调会立即被包装成微任务推入队列待执行。
  • 如果 Promise 还是 pending 状态,回调会被存储在 Promisereactions_or_result 字段中,等待 Promise 状态变更时再入队。

三、微任务(控制恢复的时机)

现在 Promise 知道要恢复了,但为什么不能立刻恢复?因为要保证 JavaScript 的执行顺序,这就靠 微任务队列 (Microtask Queue)。

  1. 为什么需要微任务?

JavaScript 是单线程的。当 bar().resolve() 被调用时,当前的代码可能还没执行完(比如正在执行某个 for 循环)。我们不能直接打断当前代码去执行 async 函数的后续逻辑。

解决方案:把 “恢复 Generator” 这件事包装成一个 微任务,放到队列里排队。

  1. 完整的调度流程(对应源码)
  • Promise 决议:bar()Promise 调用 ResolvePromise
  • 入队微任务:V8 创建一个 PromiseReactionJob(在 src/objects/promise-job.cc),推入 MicrotaskQueue(在 src/execution/microtask-queue.h)。
  • 事件循环检查:当当前的同步代码(宏任务)执行完毕,调用栈为空时,事件循环(Event Loop)会执行 MicrotaskQueue::RunMicrotasks()
  • 执行恢复:微任务被取出,执行 generator.next(value),继而执行 JSGeneratorObject::Resume,从而重新激活 JSGeneratorObject

总之,async/await 之所以能非阻塞,靠的是 V8 的微任务系统。

完整的执行链路

我们把上面的例子走一遍,看看三个角色是如何配合的:

  1. 调用 foo()
  • V8 创建一个 JSAsyncFunctionObject(继承自 JSGeneratorObject),continuation = 0(初始状态)。
  • 同时创建关联的 JSPromise,用于 foo() 的返回值。
  • foo() 推入调用栈执行。
  1. 从开头执行到 await bar()
  • 执行 const a = 1,将 a 存入寄存器。
  • 执行 bar(),得到一个已 fulfilledPromise,其值为 ‘done’。
  1. 处理 await
  • 解释器遇到 Await 字节码,调用内置的 AsyncFunctionAwait builtin(Builtins::kAsyncFunctionAwait)。
  • AsyncFunctionAwait 做三件事:
    1. 调用 PromiseResolve 确保 bar() 的返回值是 Promise
    2. 调用 PerformPromiseThen,将恢复函数注册为 Promise 的 .then 回调
    • 因为 Promise 已经是 fulfilled 状态,立即将 onFulfilled 回调包装为 PromiseReactionJob
    • 将任务推入微任务队列
    1. 返回并触发 SuspendGenerator 字节码:
    • 保存现场(continuation = 1、寄存器中的 a = 1)
    • foo() 从物理调用栈退出,返回其关联的 JSPromise
  1. 继续执行同步代码
  • 如果 foo() 后面还有同步代码,继续执行。
  • 调用栈彻底清空。
  1. 事件循环接管
  • 事件循环检测到调用栈为空,开始处理微任务队列。
  • 执行 MicrotaskQueue::RunMicrotasks()
  1. 恢复执行
  • 从队列中取出 PromiseReactionJob 并执行。
  • 调用 V8 内置的 AsyncFunctionAwaitResume builtin(Builtins::kAsyncFunctionAwaitResume)。
  • 通过 V8 Runtime 进入 C++ 层,调用 JSGeneratorObject::Resume
    • 读取 continuation = 1(字节码偏移量)
    • parameters_and_registers 恢复 a = 1 到寄存器
    • 将 await 的返回值(‘done’)放入累加器寄存器
  • 触发 ResumeGenerator 字节码:跳转到 await 之后的代码位置(状态 1)。
  • foo() 重新进入调用栈。
  • 累加器中的值被赋给 result 变量:const result = 'done'
  • 执行 const b = 2return a + b
  1. 最终结果
  • 状态机对象执行完成,continuation 设为 -1(已关闭)。
  • 计算 1 + 2 = 3,结果被包装成 Promise 对外 resolve。
  • 触发 foo().then(res => console.log(res)),输出 3

过程总结

[ 你写的代码 ]
|
| 调用 foo()
[ V8 创建 JSAsyncFunctionObject (继承自 JSGeneratorObject) ]
|
| continuation = 0 (初始状态状态 0)
| 创建关联的 JSPromise用于 foo() 的返回值
| 推入调用栈执行
[ 执行 foo 内同步代码状态 0) ]
|
| const a = 1a 变量赋值并存入寄存器
| 调用 bar() 拿到 Promise P状态为 fulfilled值是 'done'
[ 遇到 await 关键字 ]
|
| 1. 调用 AsyncFunctionAwait builtin
| - 内部通过 Await() 函数调用 PerformPromiseThen
| - 因为 Promise P 已经是 fulfilled 状态
| - 立即将 onFulfilled 回调包装为 PromiseReactionJob
| - 推入 MicrotaskQueue此时 foo 仍在调用栈
| 2. SuspendGenerator 字节码
| - 保存现场寄存器 -> parameters_and_registers (),保存 a=1
| - 标记断点continuation = 1下次从状态 1 开始
| - 退栈foo 从物理调用栈消失
[ 继续执行主线程剩余同步代码 ]
|
| 调用栈彻底清空
[ 事件循环 (Event Loop) 接管 ]
|
| 检查 MicrotaskQueue
[ 执行 MicrotaskQueue::RunMicrotasks() ]
|
| 从队列中取出 PromiseReactionJob
[ 触发内部回调V8 层面) ]
|
| 调用恢复函数类似 generator.next('done') 的底层实现
| 传入 'done' 作为 await 的返回值存储到累加器寄存器
| 进入 C++ Runtime
[ 调用 JSGeneratorObject::Resume() ]
|
| 1. 检查状态continuation = 1有效表示从 await 恢复
| 2. 重建战场parameters_and_registers -> 寄存器恢复 a=1
| 3. 准备输入累加器中已有 'done'await 的返回值
| 4. 重新入栈foo 重新出现在调用栈
[ 解释器执行 ResumeGenerator 字节码 ]
|
| Switch(continuation=1):跳转到 await 之后的代码位置
| 累加器中的值'done'被赋给 result 变量
[ 恢复执行 foo 剩余代码状态 1) ]
|
| const b = 2b 变量赋值
| return a + b计算 1 + 2 = 3
| 状态机执行完成continuation = -1已关闭
| foo 再次退栈
| JSPromise 状态变为 fulfilled值为 3
[ foo().then() 的回调被触发 ]
|
| 输出 3
[ 流程结束 ]

上面通过一个例子,简单分析了 JavaScript 内部(V8 实现)的无栈协程的运行原理。

考虑到本人并不擅长 C++,梳理过程也非常不易,该章节已经尽我所能,尽量将内容组织得足够清晰易读。 注:本章节最新于 2026 做了修订,混杂了不同版本的内容,但是不影响对核心流程的理解。最新情况请自行通过源码核对。

通过编译转换实现

async / await 未得到原生支持之前,通常可以通过转译成状态机来做支持。大致原理下面简单演示下。

有未转换的原始代码:

async function fetchUser(id: number) {
const response = await fetch(`/api/user/${id}`)
const user = await response.json()
return user
}
// 使用
fetchUser(1).then(user => console.log(user))

fetchUser 可以通过转译手段,转换成以下这样的形式:

function fetchUser(id) {
return new Promise((resolve, reject) => {
let state = 0
let user
let response
function step(result) {
try {
// 检查错误
if (result && result.error) {
state = -1
reject(result.error)
return
}
switch (state) {
case 0:
state = 1
Promise.resolve(fetch(`/api/user/${id}`))
.then(data => step({ value: data }))
.catch(error => step({ error }))
break
case 1:
response = result.value
state = 2
Promise.resolve(response.json())
.then(data => step({ value: data }))
.catch(error => step({ error }))
break
case 2:
user = result.value
state = -1
resolve(user)
break
}
} catch (error) {
state = -1
reject(error)
}
}
step() // 初始调用,result 为 undefined 但 case 0 不使用它
})
}

本质上是通过记录执行位置,再使用 Promise 链接各个步骤,实现自动化的异步流程控制。

其他语言的 async/await

各种语言的 async/await 实现原理大多类似,其实看到 async/await 关键字,就能立马判断出这是一种无栈协程了。 而无栈协程的实现也基本都是大同小异,通常都是编译成状态机(更底层),或者利用语言自身特性编译成生成器(更便捷)。

我们可以简单看看一些主流的语言的 async/await 是怎么设计的,以便加深对这一机制的理解。

Python

async def fetch_data():
await asyncio.sleep(1)
return {"data": "value"}

基于事件循环实现,编译器将 async def 函数转换为原生协程对象(coroutine object)。 虽然早期版本基于生成器(yield from),但最新的 Python 版本,async/await 已成为独立的语言特性,与生成器类型不同。

C#

public async Task<User> GetUserAsync(int id)
{
var user = await _userRepository.FindAsync(id);
var posts = await _postRepository.GetByUserIdAsync(id);
user.Posts = posts;
return user;
}

编译器将 async 方法转换为状态机(实现 IAsyncStateMachine 接口)。

Rust

async fn fetch_user(id: u32) -> User {
let user = db::find_user(id).await?;
let posts = db::find_posts(user.id).await?;
User { user, posts }
}

编译器将 async fn 转换为生成器(generator),再自动包装成实现 Future trait 的匿名类型, 追求”零成本抽象”(Zero-Cost Abstraction)。

C++20

// Task 是自定义的返回类型(需库提供或自行实现)
Task my_coroutine() {
std::cout << "Step 1\n";
co_await std::suspend_always{};
std::cout << "Step 2\n";
}

编译器生成协程帧(Coroutine Frame),通过 promise_type 定义协程行为。 注意:C++20 需要自定义或使用库提供的返回类型(如 TaskGenerator 等)。

核心思想都一样:编译器把你的异步代码转换成状态机,在 await 点暂停和恢复


性能考虑

JavaScript 的 async/await 虽然方便,但使用不当也会带来性能开销:

// 不必要的 async,不要这样做
async function getValue() {
return 42
}
// 直接返回,这是更合适的做法
function getValue() {
return 42
}
// 串行 await,通常这是不合理的,没有数据依赖的多个异步串行,非常慢
async function fetchMultiple() {
const a = await fetch(url1)
const b = await fetch(url2)
const c = await fetch(url3)
return [a, b, c]
}
// 并行 await,通常这样做是更合理的,多个无相互依赖的任务并发执行
async function fetchMultiple() {
const [a, b, c] = await Promise.all([
fetch(url1),
fetch(url2),
fetch(url3)
])
return [a, b, c]
}

参考资源


全文完