async和await
1. async/await 基础
async/await 是 JavaScript 中处理异步操作的语法糖,它让异步代码看起来像同步代码,极大提升了可读性和可维护性。其底层原理完全基于 Promise 和 微任务(microtask)机制。
一、基本语法规则
async function foo() {
const result = await somePromise();
console.log(result);
}
async函数总是返回一个 Promise。await只能在async函数内部使用。await后面可以跟:- 一个 Promise(最常见)
- 任意值(如字符串、数字等),会被自动包装成已决议的 Promise(
Promise.resolve(value))
二、核心原理:基于 Promise 的状态机
1. async 函数的本质
async function f() {
return 'hello';
}
// 等价于
function f() {
return Promise.resolve('hello');
}
✅ 所有
async函数都会被隐式包装成Promise。
如果函数内部抛出异常:
async function f() {
throw new Error('Oops');
}
// 等价于
function f() {
return Promise.reject(new Error('Oops'));
}
2. await 的本质:暂停 + 注册 .then
await expr 的行为可以理解为:
- 对
expr调用Promise.resolve(expr),确保它是一个 Promise。 - 暂停当前 async 函数的执行(但不阻塞 JS 主线程!)。
- 将
await之后的代码(即“恢复执行的部分”)注册为该 Promise 的.then回调。 - 一旦 Promise 状态变为 fulfilled 或 rejected,这个回调就会被放入微任务队列,等待执行。
🔑 关键点:
await并不会“阻塞线程”,而是通过 Promise 的链式回调实现“逻辑上的暂停”。
三、执行流程示例(含事件循环)
看一个经典例子:
console.log('1');
async function async1() {
console.log('2');
await async2();
console.log('3');
}
async function async2() {
console.log('4');
}
async1();
console.log('5');
执行顺序分析:
- 打印
'1' - 调用
async1()→ 进入函数,打印'2' - 执行
await async2():- 调用
async2()→ 打印'4',并返回Promise.resolve(undefined) await遇到这个 Promise,于是:- 暂停
async1的后续代码(console.log('3')) - 将
console.log('3')包装成微任务,放入微任务队列
- 暂停
- 调用
- 继续执行主栈:打印
'5' - 主宏任务结束 → 清空微任务队列 → 执行
console.log('3')
✅ 输出顺序:1 → 2 → 4 → 5 → 3
注意:
'3'是在微任务中执行的,所以排在'5'之后。
四、错误处理机制
await 会“解包” Promise 的结果:
- 如果 Promise 成功(fulfilled),
await返回其值。 - 如果 Promise 失败(rejected),
await会抛出异常,可用try...catch捕获。
async function f() {
try {
const data = await Promise.reject('Error!');
console.log(data);
} catch (err) {
console.log('Caught:', err); // Caught: Error!
}
}
这比 .catch() 更接近同步错误处理习惯。
五、性能与微任务的影响
由于 await 依赖微任务,连续使用 await 会导致多个微任务排队:
async function test() {
await 1;
await 2;
await 3;
console.log('done');
}
等价于:
Promise.resolve(1)
.then(() => Promise.resolve(2))
.then(() => Promise.resolve(3))
.then(() => console.log('done'));
每次 await 都会引入一次微任务调度,虽然开销极小,但在极端性能敏感场景需注意。
六、总结
async/await是 Promise 的语法糖。async函数返回 Promise。await会:- 将表达式转为 Promise;
- 暂停函数执行;
- 把后续代码作为
.then回调放入微任务队列。
- 所有执行仍在 JS 主线程 上,依赖 事件循环 + 微任务机制 实现“看似同步”的异步控制流。
如果你对 await 在 V8 引擎中的具体实现(如状态机生成、字节码转换)感兴趣,也可以进一步探讨!
2. async/await 历史演进
async/await在语义上是 Promise 的语法糖,但在实现历史上和语言设计上,它确实脱胎于 Generator 函数。现代 JavaScript 引擎中,async/await已不再依赖用户态的 Generator,但其控制流思想(暂停/恢复)直接继承自 Generator。
####### 一、历史背景:从回调地狱 → Promise → Generator → async/await
######## 1. 回调地狱(Callback Hell)
fs.readFile('1.txt', (err, data1) => {
fs.readFile('2.txt', (err, data2) => {
fs.readFile('3.txt', (err, data3) => {
// ...
});
});
});
######## 2. Promise 改善链式调用
readFile('1.txt')
.then(data1 => readFile('2.txt'))
.then(data2 => readFile('3.txt'));
✅ 解决了嵌套,但逻辑仍不“线性”。
######## 3. Generator + Promise:手动实现“暂停” ES6 引入了 Generator 函数,可以暂停和恢复执行:
function* main() {
const data1 = yield readFile('1.txt');
const data2 = yield readFile('2.txt');
console.log(data1, data2);
}
但 Generator 本身不会自动处理异步,需要一个“运行器”(如 co 库)来驱动:
co(main); // co 库会自动调用 .next(),并在 Promise resolve 后传回值
✅ 这已经非常接近
async/await的写法!
######## 4. async/await:官方内置的“自动运行器” ES2017 直接将这种模式标准化:
- 用
async替代function* - 用
await替代yield - 引擎自动处理 Promise 和恢复逻辑,无需外部运行器
所以:async/await 是对 “Generator + Promise + 自动运行器” 模式的官方封装。
####### 二、原理上的联系:控制流模型相同
| 特性 | Generator | async/await |
|---|---|---|
| 可暂停执行 | ✅ yield | ✅ await |
| 可恢复执行 | ✅ .next(value) | ✅ Promise resolve 后自动恢复 |
| 返回值传递 | 通过 .next(resolvedValue) 传入 | 通过 await 表达式获取 Promise 结果 |
| 错误传递 | .throw(err) | Promise reject → throw in await |
💡 两者都实现了 协作式多任务(cooperative multitasking) 的控制流:函数主动让出控制权,等待外部条件满足后再继续。
####### 三、关键区别:谁在驱动?
| 方面 | Generator | async/await |
|---|---|---|
| 驱动者 | 用户代码(需手动调用 .next() 或使用 co 等库) | JavaScript 引擎(自动集成事件循环) |
| 返回值类型 | Iterator 对象 | Promise |
| 错误处理 | 需手动 .throw() | 自动将 reject 转为异常 |
| 与 Promise 绑定 | 松耦合(可 yield 任意值) | 紧耦合(await 自动解包 Promise) |
######## 示例对比
######### Generator + co(旧方案)
const co = require('co');
function* fetchUser() {
const user = yield fetch('/user').then(r => r.json());
const posts = yield fetch(`/posts?user=${user.id}`).then(r => r.json());
return { user, posts };
}
co(fetchUser).then(result => console.log(result));
######### async/await(现代方案)
async function fetchUser() {
const user = await fetch('/user').then(r => r.json());
const posts = await fetch(`/posts?user=${user.id}`).then(r => r.json());
return { user, posts };
}
fetchUser().then(result => console.log(result));
🔁
async/await把co的逻辑内置到语言中了。
####### 四、现代引擎中的实现:还用 Generator 吗?
早期提案(如 ES7 草案)确实建议用 Generator 实现 async/await,但现代 JS 引擎(如 V8)已不再这样做。
######## 原因:
- 性能:直接编译为状态机比包装 Generator 更高效。
- 语义清晰:
async函数天然返回 Promise,而 Generator 返回 Iterator,类型不匹配。 - 错误处理复杂:Generator 的
.throw()机制与 Promise reject 的集成较复杂。
######## 实际实现方式(V8):
- 将
async函数编译为一个内部状态机(state machine)。 - 每个
await点对应一个状态。 - 使用 Promise 链和微任务调度状态切换。
- 完全绕过用户可见的 Generator 机制。
✅ 所以:今天
async/await在实现上已独立于 Generator,但设计思想源自它。
####### 五、总结:关系图谱
回调地狱
↓
Promise(解决链式调用)
↓
Generator + Promise + 运行器(实现“伪同步”)
↓
async/await(官方标准化,内置运行器,返回 Promise)
- 语义上:
async/await是 Promise 的语法糖。 - 思想上:它继承了 Generator 的“暂停/恢复”控制流模型。
- 实现上:现代引擎用状态机直接实现,不再依赖 Generator。
为什么现代引擎不再用 Generator 实现 async/await?
现代 JavaScript 引擎(如 V8、SpiderMonkey、JavaScriptCore)在早期探索阶段确实考虑过用 Generator 实现 async/await,但最终选择了更直接的“状态机 + Promise”方案。原因主要包括以下几点:
一、性能开销:Generator 有额外抽象层
1. Generator 本身有运行时成本
- 每个 Generator 函数调用都会创建一个 Iterator 对象,包含:
- 内部状态(如当前执行到哪个
yield) - 闭包环境(保存局部变量)
- 方法(
.next(),.throw(),.return())
- 内部状态(如当前执行到哪个
- 这些对象需要内存分配和垃圾回收。
2. async/await 不需要完整的 Iterator 接口
async函数只关心“暂停后自动恢复”,不需要用户手动调用.next()。- 返回的是
Promise,而不是Iterator。 - 引入 Generator 相当于“为了实现 A,强行套用 B 的完整机制”,造成不必要的抽象开销。
✅ 直接编译为状态机可以避免这些中间对象,提升性能和内存效率。
二、语义不匹配:类型系统冲突
| 特性 | Generator 函数 | async 函数 |
|---|---|---|
| 返回值类型 | Iterator(可迭代对象) | Promise |
| 错误传播 | 通过 .throw() 手动注入 | 自动将 reject 转为异常 |
| 控制流驱动 | 外部调用者驱动(pull model) | 引擎自动驱动(push model) |
如果强行用 Generator 实现 async/await,就需要:
- 把 Iterator 包装成 Promise;
- 在 Promise resolve 时自动调用
.next(); - 在 reject 时调用
.throw(); - 处理
return和throw的边界情况。
这不仅复杂,还容易出错。
🚫 语义错位导致实现脆弱、难以优化。
三、优化困难:阻碍 JIT 编译器优化
现代 JS 引擎依赖 JIT(Just-In-Time)编译器(如 V8 的 TurboFan)对热点代码进行深度优化。
Generator 阻碍优化的原因:
- 控制流不透明:JIT 难以静态分析
yield点之间的数据流。 - 闭包逃逸:Generator 必须将所有局部变量保存在堆上(而非栈上),因为函数可能在多次恢复中访问它们。
- 状态切换开销:每次
.next()调用都涉及函数调用和上下文切换。
而直接编译为状态机:
- 控制流是显式的 switch-case 或跳转表;
- 局部变量可以更高效地分配(部分可保留在寄存器);
- JIT 可以像优化普通函数一样优化 async 函数。
✅ 状态机模型更贴近底层,利于高性能编译。
四、规范与兼容性:避免耦合
ECMAScript 规范希望 async/await 是一个独立、自包含的特性,而不是“Generator 的特例”。
- 如果依赖 Generator,那么:
- 修改 Generator 行为会影响
async/await; - 需要保证两者语义严格一致;
- 增加规范复杂度。
- 修改 Generator 行为会影响
而独立实现:
- 语义清晰:
async→ 返回 Promise,await→ 解包 Promise; - 与现有 Promise 生态无缝集成;
- 更容易被开发者理解和调试。
五、实际证据:V8 的实现方式
V8 引擎对 async/await 的处理流程如下:
- 解析阶段:识别
async函数和await表达式。 - 字节码生成:将函数体转换为带状态标签的字节码(类似状态机)。
- 执行阶段:
- 遇到
await时,暂停执行,返回一个 Promise; - 将剩余代码注册为该 Promise 的
.then回调(微任务); - Promise resolve 后,从断点处恢复执行。
- 遇到
整个过程完全不创建 Generator 对象,也不调用任何 Iterator 协议方法。
🔍 你可以通过 V8 的 Ignition 字节码 查看
Await指令的原生支持。
六、总结:为什么不用 Generator?
| 原因 | 说明 |
|---|---|
| 性能 | Generator 有额外对象创建和闭包开销 |
| 语义 | 返回类型(Iterator vs Promise)不匹配 |
| 优化 | 状态机更利于 JIT 编译器优化 |
| 简洁性 | 独立实现更符合“最小惊讶原则” |
| 工程实践 | 现代引擎已验证状态机方案更高效可靠 |
💡 Generator 是
async/await的“思想先驱”,但不是它的“实现基础”。
3. V8 如何将 async/await 编译成状态机?
V8 引擎将 async/await 编译为内部状态机(state machine),但这个过程不会生成用户可见的 JavaScript 代码(比如 switch 语句),而是在 字节码(Bytecode)和内部表示(IR)层面完成的。下面从编译流程、状态机建模、字节码行为三个层面详细解析。
一、整体思路:每个 await 是一个状态
V8 将 async 函数体按 await 表达式切分为多个连续的执行阶段,每个阶段对应一个状态编号(state ID)。函数执行时:
- 遇到
await→ 暂停当前状态,返回一个 Promise; - 当该 Promise resolve 后 → 触发微任务,恢复到下一个状态继续执行。
这本质上是一个线性状态机,状态转移由 Promise 的完成驱动。
二、编译流程(V8 内部)
步骤 1:解析(Parsing)
- 识别
async函数和所有await表达式。 - 记录每个
await的位置和依赖关系。
步骤 2:作用域分析 & 变量提升
- 所有在
await之后仍被使用的变量,必须保活(不能被 GC)。 - V8 会将这些变量放入一个闭包上下文(context) 或堆分配的槽(slot) 中。
步骤 3:生成状态机逻辑(在字节码生成阶段)
- V8 的 Ignition 字节码生成器 会为
async函数生成特殊的控制流。 - 每个
await对应一个 挂起点(suspension point),并分配一个状态 ID。 - 函数入口处会检查“是否是 resume 调用”,并跳转到对应状态。
💡 虽然你看不到
switch(state),但字节码的跳转逻辑等价于状态机。
三、概念性等价转换(帮助理解)
虽然 V8 不输出 JS 代码,但我们可以手动写出语义等价的状态机版本:
原始 async/await 代码:
async function fetchUser() {
console.log('Start');
const user = await fetch('/user');
console.log('Got user');
const posts = await fetch(`/posts?uid=${user.id}`);
return posts;
}
等价的状态机实现(V8 内部逻辑的 JS 模拟):
function fetchUser() {
// 状态:0=初始, 1=等待 user, 2=等待 posts
let state = 0;
let user; // 需要跨 await 保活的变量
function resume(value) {
switch (state) {
case 0:
console.log('Start');
state = 1;
return fetch('/user').then(resume); // await 第一次
case 1:
user = value;
console.log('Got user');
state = 2;
return fetch(`/posts?uid=${user.id}`).then(resume); // await 第二次
case 2:
return Promise.resolve(value); // return posts
}
}
return resume(); // 启动状态机
}
✅ 这就是 V8 内部状态机的语义模型。
四、V8 字节码中的关键指令
V8 的 Ignition 字节码为 async/await 提供了原生支持,核心指令包括:
| 指令 | 作用 |
|---|---|
CreateResumptionFunction | 创建用于 resume 的闭包函数(即上面的 resume) |
Await | 挂起当前执行,将值包装为 Promise,并注册 resume 回调 |
JumpIfNotSmi / Star | 用于状态跳转和变量保存 |
简化字节码示意(概念版):
AsyncFunctionEntry
LdaZero ; state = 0
Star r0 ; 保存状态到寄存器
State0:
CallRuntime ConsoleLog "Start"
CallRuntime Fetch "/user"
Await ; ← 挂起!返回 Promise,注册 resume
; (如果被 resume,从这里继续)
State1:
Star r1 ; user = value
CallRuntime ConsoleLog "Got user"
LdaNamedProperty r1, "id"
CallRuntime Fetch ...
Await ; ← 再次挂起
State2:
Return ; 返回最终值
🔑
Await指令会:
- 调用
Promise.resolve(value);- 创建一个微任务回调(即 resume 函数);
- 立即返回外层 Promise;
- 当微任务执行时,从断点恢复。
五、变量保活与上下文
由于函数可能在 await 后恢复,V8 必须确保:
- 所有跨
await使用的变量不会被优化掉; - 这些变量被存储在堆上的上下文对象中(类似闭包)。
例如:
async function f() {
let a = 1;
await something();
console.log(a); // a 必须在 await 后仍然可访问
}
→ V8 会将 a 分配到堆上,而不是栈上。
六、错误处理:状态机如何处理 reject?
当 await 的 Promise 被 reject 时:
- V8 会将 reject 原因作为异常抛出;
- 如果在
try...catch中,会跳转到 catch 块对应的状态; - 否则,Promise 链会进入 rejected 状态。
这同样通过状态机中的异常处理表(exception handler table) 实现。
七、为什么这种设计高效?
- 线性控制流:
async/await是单向的(不能像 Generator 那样任意.next()),状态转移可预测。 - 微任务集成:直接利用事件循环的微任务队列,无需额外调度器。
- JIT 友好:TurboFan(V8 的优化编译器)可以对状态机进行内联和去虚拟化优化。
八、验证:如何查看 V8 的字节码?
你可以用 V8 的调试工具查看真实字节码:
# 安装 d8(V8 的命令行 shell)
git clone https://chromium.googlesource.com/v8/v8
cd v8
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8
# 打印字节码
echo "
async function f() {
await 1;
return 2;
}
" | out.gn/x64.release/d8 --print-bytecode --allow-natives-syntax
你会看到类似:
[generated bytecode for function: f]
...
14 E> 0x35a7e8a9a4e2 @ 0 : 95 CreatePromise
16 E> 0x35a7e8a9a4e3 @ 1 : 26 f9 Star r0
18 E> 0x35a7e8a9a4e5 @ 3 : 96 CreateResumptionFunction
20 E> 0x35a7e8a9a4e6 @ 4 : 26 fa Star r1
22 E> 0x35a7e8a9a4e8 @ 6 : 0d LdaZero
24 E> 0x35a7e8a9a4e9 @ 7 : 26 fb Star r2
26 E> 0x35a7e8a9a4eb @ 9 : 97 Await
...
其中 Await 和 CreateResumptionFunction 就是状态机的核心。
总结
| 问题 | 答案 |
|---|---|
V8 是否将 async/await 编译成状态机? | ✅ 是,在字节码层面实现 |
是否生成 switch(state) JS 代码? | ❌ 否,是内部表示 |
| 状态如何表示? | 通过状态 ID 和字节码偏移量 |
| 如何恢复执行? | 通过微任务调用 resumption function |
| 变量如何保活? | 分配到堆上下文,防止 GC |
💡
async/await的状态机是 V8 高性能异步执行的关键,它平衡了语义清晰性、执行效率和与事件循环的无缝集成。
4. V8 如何编译Generator?
V8 引擎对 Generator 函数 的编译和执行采用了与普通函数完全不同的策略,核心目标是支持 暂停(suspend)与恢复(resume) 的语义。虽然它不使用传统 switch-case 状态机,但通过 字节码切分 + 上下文保存 + 程序计数器(PC)恢复 实现了 Generator 的控制流。
下面我们从 编译流程、运行时机制、字节码行为、内存模型 四个维度详细解析 V8 如何编译和执行 Generator。
一、核心挑战:如何“暂停”一个函数?
普通函数一旦调用,就会一直执行到 return 或抛出异常。
但 Generator 需要:
- 在
yield处暂停执行; - 保留所有局部变量状态;
- 后续通过
.next()从暂停点继续执行。
这要求 V8 必须打破“栈帧一次性执行”的模型。
二、V8 编译 Generator 的关键步骤
1. 解析阶段:识别 yield 表达式
- V8 的 parser 会标记所有
yield和yield*的位置。 - 记录每个
yield的作用域信息和表达式依赖。
2. 作用域分析:变量提升到堆上下文
由于 Generator 可能多次进出,其局部变量不能放在栈上(栈帧在暂停时会被销毁),因此:
- 所有在
yield之后仍被引用的变量,都会被提升到一个 堆分配的上下文对象(Context Object) 中。 - 这类似于闭包,但由引擎自动管理。
✅ 例如:
function* gen() {
let x = 1;
yield x;
console.log(x); // x 必须在 yield 后仍可访问 → 提升到堆
}
3. 字节码生成:插入 Suspend 指令
V8 的 Ignition 字节码生成器会:
- 将函数体按
yield点分割为多个连续的基本块(basic blocks); - 在每个
yield处插入特殊的SuspendGenerator字节码指令; - 为每个 resume 点生成对应的入口标签(resume point)。
三、运行时机制:如何暂停与恢复?
1. Generator 对象的结构
每次调用 Generator 函数(如 gen()),V8 会创建一个 Generator 对象,内部包含:
- 字节码数组(Bytecode Array):整个函数的字节码;
- 当前程序计数器(PC):记录上次暂停的位置;
- 上下文对象(Context):保存所有局部变量;
- 状态标志:
suspendedStart,suspendedYield,executing,completed等。
2. 首次调用 .next()
- V8 从字节码起始位置开始解释执行;
- 遇到
SuspendGenerator指令时:- 保存当前 PC(即下一条指令地址);
- 将
yield表达式的值作为返回值; - 将状态设为
suspendedYield; - 返回控制权给调用者。
3. 后续调用 .next(value)
- V8 检查 Generator 状态是否为
suspendedYield; - 将传入的
value压入寄存器(作为yield表达式的返回值); - 从保存的 PC 位置继续执行字节码;
- 直到遇到下一个
yield或return。
🔑 关键:没有状态变量或 switch,而是靠真实的程序计数器(PC)恢复执行流。
四、字节码示例(简化概念版)
考虑以下 Generator:
function* counter() {
let i = 0;
yield i++;
yield i++;
}
V8 生成的字节码(概念示意)如下:
; 初始化
LdaZero ; 加载 0
Star r0 ; i = 0(实际存入堆上下文)
; 第一个 yield 前
LdaContextSlot i ; 从上下文加载 i
Inc ; i++
StaContextSlot i ; 存回上下文
SuspendGenerator ; ← 挂起!保存 PC,返回当前值
; Resume Point 1(第一次 .next() 后从此处开始)
LdaContextSlot i
Inc
StaContextSlot i
SuspendGenerator ; ← 再次挂起
; Resume Point 2
ReturnUndefined ; 结束
SuspendGenerator是一个特殊指令,会:- 保存当前字节码偏移量(PC);
- 返回
{ value: ..., done: false }; - 暂停执行。
五、与 async/await 的关键区别
| 特性 | Generator | async/await |
|---|---|---|
| 驱动方式 | 外部 .next()(pull model) | Promise resolve 自动触发(push model) |
| 返回值类型 | Iterator | Promise |
| 错误注入 | .throw(err) | reject → throw |
| 编译模型 | 字节码挂起/恢复(基于 PC) | 状态机(基于状态 ID + Promise 链) |
| 变量存储 | 全部提升到堆上下文 | 仅跨 await 变量需保活 |
💡 Generator 更“底层”,async/await 更“高层”。
六、性能优化:TurboFan 如何处理 Generator?
V8 的优化编译器 TurboFan 对 Generator 的优化有限,因为:
- 控制流不可预测(用户可任意调用
.next()); - 变量必须逃逸到堆上;
- 难以内联或去虚拟化。
因此,热点 Generator 通常不会被深度优化,而 async/await 因其线性控制流更容易被 TurboFan 优化。
七、验证:查看真实字节码
使用 V8 的 d8 工具:
// test.js
function* gen() {
yield 1;
yield 2;
}
运行:
d8 --print-bytecode test.js
你会看到类似输出:
[generators bytecode]
...
10 E> 0x... @ 0 : 90 CreateGenerator
12 E> 0x... @ 1 : a4 SuspendGenerator
14 E> 0x... @ 2 : 0c LdaSmi [1]
16 E> 0x... @ 3 : a4 SuspendGenerator
...
其中:
CreateGenerator:创建 Generator 对象;SuspendGenerator:挂起点。
八、总结
V8 编译 Generator 的核心思想是:
将函数体视为可中断的字节码流,通过保存程序计数器(PC)和堆上下文,在多次调用
.next()时精确恢复执行状态。
它不是状态机,而是一种轻量级协程(coroutine)模拟,依赖:
- 字节码解释器的 PC 机制;
- 堆分配的上下文对象;
- 特殊的
SuspendGenerator指令。
这种设计既满足了 ES6 规范的语义要求,又在 V8 的架构中实现了高效执行。
如果你希望我对比 Generator 和 async/await 在 V8 中的字节码差异,或者展示一个完整的调试流程,也可以继续深入!