跳到主要内容

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 的行为可以理解为:

  1. expr 调用 Promise.resolve(expr),确保它是一个 Promise。
  2. 暂停当前 async 函数的执行(但不阻塞 JS 主线程!)。
  3. await 之后的代码(即“恢复执行的部分”)注册为该 Promise 的 .then 回调。
  4. 一旦 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. 打印 '1'
  2. 调用 async1() → 进入函数,打印 '2'
  3. 执行 await async2()
    • 调用 async2() → 打印 '4',并返回 Promise.resolve(undefined)
    • await 遇到这个 Promise,于是:
      • 暂停 async1 的后续代码(console.log('3')
      • console.log('3') 包装成微任务,放入微任务队列
  4. 继续执行主栈:打印 '5'
  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/awaitPromise 的语法糖
  • 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 + 自动运行器” 模式的官方封装。


####### 二、原理上的联系:控制流模型相同

特性Generatorasync/await
可暂停执行yieldawait
可恢复执行.next(value)✅ Promise resolve 后自动恢复
返回值传递通过 .next(resolvedValue) 传入通过 await 表达式获取 Promise 结果
错误传递.throw(err)Promise reject → throw in await

💡 两者都实现了 协作式多任务(cooperative multitasking) 的控制流:函数主动让出控制权,等待外部条件满足后再继续。


####### 三、关键区别:谁在驱动?

方面Generatorasync/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/awaitco 的逻辑内置到语言中了。


####### 四、现代引擎中的实现:还用 Generator 吗?

早期提案(如 ES7 草案)确实建议用 Generator 实现 async/await,但现代 JS 引擎(如 V8)已不再这样做

######## 原因:

  1. 性能:直接编译为状态机比包装 Generator 更高效。
  2. 语义清晰async 函数天然返回 Promise,而 Generator 返回 Iterator,类型不匹配。
  3. 错误处理复杂: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()
  • 处理 returnthrow 的边界情况。

这不仅复杂,还容易出错。

🚫 语义错位导致实现脆弱、难以优化


三、优化困难:阻碍 JIT 编译器优化

现代 JS 引擎依赖 JIT(Just-In-Time)编译器(如 V8 的 TurboFan)对热点代码进行深度优化。

Generator 阻碍优化的原因:

  1. 控制流不透明:JIT 难以静态分析 yield 点之间的数据流。
  2. 闭包逃逸:Generator 必须将所有局部变量保存在堆上(而非栈上),因为函数可能在多次恢复中访问它们。
  3. 状态切换开销:每次 .next() 调用都涉及函数调用和上下文切换。

而直接编译为状态机:

  • 控制流是显式的 switch-case 或跳转表
  • 局部变量可以更高效地分配(部分可保留在寄存器);
  • JIT 可以像优化普通函数一样优化 async 函数。

状态机模型更贴近底层,利于高性能编译


四、规范与兼容性:避免耦合

ECMAScript 规范希望 async/await 是一个独立、自包含的特性,而不是“Generator 的特例”。

  • 如果依赖 Generator,那么:
    • 修改 Generator 行为会影响 async/await
    • 需要保证两者语义严格一致;
    • 增加规范复杂度。

而独立实现:

  • 语义清晰:async → 返回 Promise,await → 解包 Promise;
  • 与现有 Promise 生态无缝集成;
  • 更容易被开发者理解和调试。

五、实际证据:V8 的实现方式

V8 引擎对 async/await 的处理流程如下:

  1. 解析阶段:识别 async 函数和 await 表达式。
  2. 字节码生成:将函数体转换为带状态标签的字节码(类似状态机)。
  3. 执行阶段
    • 遇到 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 指令会:

  1. 调用 Promise.resolve(value)
  2. 创建一个微任务回调(即 resume 函数);
  3. 立即返回外层 Promise;
  4. 当微任务执行时,从断点恢复。

五、变量保活与上下文

由于函数可能在 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) 实现。


七、为什么这种设计高效?

  1. 线性控制流async/await 是单向的(不能像 Generator 那样任意 .next()),状态转移可预测。
  2. 微任务集成:直接利用事件循环的微任务队列,无需额外调度器。
  3. 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
...

其中 AwaitCreateResumptionFunction 就是状态机的核心。


总结

问题答案
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 会标记所有 yieldyield* 的位置。
  • 记录每个 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 位置继续执行字节码
  • 直到遇到下一个 yieldreturn

🔑 关键:没有状态变量或 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 的关键区别

特性Generatorasync/await
驱动方式外部 .next()(pull model)Promise resolve 自动触发(push model)
返回值类型IteratorPromise
错误注入.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 中的字节码差异,或者展示一个完整的调试流程,也可以继续深入!