JavaScript async/await 底层原理:从无栈协程到 V8 实现
前言
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 1console.log(gen.next()) // { value: 2, done: false }, 输出: Step 2console.log(gen.next()) // { value: 3, done: true }, 输出: Step 3Generator 的关键特性:
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 + 执行器的代码:
npm install --save-dev babel-plugin-transform-async-to-generator核心转译步骤:
- async 函数包装 - 将 async 函数转换为返回 Promise 的普通函数
- Generator 实现 - 函数体被改写为 Generator,await 变成 yield
- 自动执行器 - 生成一个递归执行器,自动驱动 Generator 运行
- Promise 链接 - 每次 yield 的值都会被包装成 Promise,完成后继续 next
这种转译方式让开发者可以在 2015-2016 年就提前使用 async/await 语法。
在 2013-2016 年间,基于 Generator 的异步编程方案非常流行。co、koa、co-body、co-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 中。
这个角色相关的事件有以下:
- 编译器的转换:从
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 = 2和return a + b,函数结束 - 状态
-1:终止状态(continuation = -1)
- 状态 0:从函数开始执行
- 状态机对象的核心字段(对应源码)在
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 继承自 JSGeneratorObjectextern class JSAsyncFunctionObject extends JSGeneratorObject { promise: JSPromise; // 关联的 Promise 对象 await_resolve_closure: JSAny; // 复用优化:resolve 回调闭包 await_reject_closure: JSAny; // 复用优化:reject 回调闭包}JSAsyncFunctionObject 继承了 JSGeneratorObject 的所有字段(包括 continuation 和 parameters_and_registers),并额外添加了 promise 字段用于关联异步函数返回的 Promise。
- 暂停时做了什么?
- 当执行到
await bar()时: - 保存数据:把局部变量
a = 1存入parameters_and_registers数组。 - 保存位置:把
continuation设为1(表示下次恢复要从 “状态 1” 开始)。 - 交出控制权:函数退出,返回一个
Promise。
二、Promise(暂停的触发器与结果载体)
await 本质上是在 “等一个 Promise”。JSGeneratorObject 负责 “暂停”,而 Promise 负责 “通知何时恢复”。
async函数的返回值
任何 async function 的返回值都是一个 Promise。这是 V8 自动包装的:
如果函数正常完成,则 Promise 变为 fulfilled。
如果函数抛出错误,则 Promise 变为 rejected。
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已经是fulfilled或rejected状态(如Promise.resolve('done')),回调会立即被包装成微任务推入队列待执行。- 如果
Promise还是pending状态,回调会被存储在Promise的reactions_or_result字段中,等待Promise状态变更时再入队。
三、微任务(控制恢复的时机)
现在 Promise 知道要恢复了,但为什么不能立刻恢复?因为要保证 JavaScript 的执行顺序,这就靠 微任务队列 (Microtask Queue)。
- 为什么需要微任务?
JavaScript 是单线程的。当 bar().resolve() 被调用时,当前的代码可能还没执行完(比如正在执行某个 for 循环)。我们不能直接打断当前代码去执行 async 函数的后续逻辑。
解决方案:把 “恢复 Generator” 这件事包装成一个 微任务,放到队列里排队。
- 完整的调度流程(对应源码)
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 的微任务系统。
完整的执行链路
我们把上面的例子走一遍,看看三个角色是如何配合的:
- 调用
foo()
- V8 创建一个
JSAsyncFunctionObject(继承自JSGeneratorObject),continuation = 0(初始状态)。 - 同时创建关联的
JSPromise,用于foo()的返回值。 - 将
foo()推入调用栈执行。
- 从开头执行到
await bar()
- 执行
const a = 1,将a存入寄存器。 - 执行
bar(),得到一个已fulfilled的Promise,其值为 ‘done’。
- 处理 await
- 解释器遇到
Await字节码,调用内置的AsyncFunctionAwaitbuiltin(Builtins::kAsyncFunctionAwait)。 AsyncFunctionAwait做三件事:- 调用
PromiseResolve确保bar()的返回值是 Promise - 调用
PerformPromiseThen,将恢复函数注册为 Promise 的.then回调
- 因为 Promise 已经是 fulfilled 状态,立即将 onFulfilled 回调包装为
PromiseReactionJob - 将任务推入微任务队列
- 返回并触发
SuspendGenerator字节码:
- 保存现场(continuation = 1、寄存器中的 a = 1)
foo()从物理调用栈退出,返回其关联的JSPromise。
- 调用
- 继续执行同步代码
- 如果
foo()后面还有同步代码,继续执行。 - 调用栈彻底清空。
- 事件循环接管
- 事件循环检测到调用栈为空,开始处理微任务队列。
- 执行
MicrotaskQueue::RunMicrotasks()。
- 恢复执行
- 从队列中取出
PromiseReactionJob并执行。 - 调用 V8 内置的
AsyncFunctionAwaitResumebuiltin(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 = 2和return a + b。
- 最终结果
- 状态机对象执行完成,
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 = 1:a 变量赋值并存入寄存器 | 调用 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 = 2:b 变量赋值 | 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 需要自定义或使用库提供的返回类型(如 Task、Generator 等)。
核心思想都一样:编译器把你的异步代码转换成状态机,在 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]}参考资源
- ECMAScript® 2024 Language Specification - Async Functions
- V8 Internals: Async Functions
- TC39 Proposal: Async/Await
- Python asyncio Documentation
- C# Task-based Asynchronous Pattern
全文完