~/ ?.log $
返回文章列表
7 min read
更新于 2026年3月4日

我不知道的 V8(16)— async/await:状态机、伪协程与微任务调度

await 让函数"暂停",等 Promise 完成后"继续"——这个描述没错,但暂停是什么意思?主线程真的停下来等了吗?V8 用什么机制实现了"看似同步、实为异步"的执行模式?答案在 V8 把 async 函数转换成的状态机里,以及await 背后的微任务调度机制里。

await 让函数”暂停”,等 Promise 完成后”继续”——这个描述没错,但暂停是什么意思?主线程真的停下来等了吗?V8 用什么机制实现了”看似同步、实为异步”的执行模式?答案在 V8 把 async 函数转换成的状态机里,以及await 背后的微任务调度机制里。

一、async 函数的本质:Promise + 状态机

V8 把 async 函数编译成两个部分:外层是一个 Promise 包装,内部是一个状态机。

原始的 async 函数:

async function fetchData() {
  const result = await Promise.resolve(42);
  console.log(result);
  return result;
}

V8 内部转换后的等效逻辑(概念性,非实际代码):

function fetchData() {
  // 外层:整个函数返回一个 Promise
  return new Promise((resolve, reject) => {
    let state = 0;
    let value;

    // 内部状态机:每个 await 是一个状态点
    function resume(result) {
      value = result;
      try {
        switch (state) {
          case 0:
            // await 之前的代码
            state = 1;
            // await Promise.resolve(42)
            // 把"继续执行"挂到 Promise 的 then 回调上
            return Promise.resolve(42).then(resume);

          case 1:
            // await 之后的代码
            console.log(value); // 42
            resolve(value); // 整个 async 函数完成
            break;
        }
      } catch (e) {
        reject(e);
      }
    }

    resume(); // 启动
  });
}

每个 await 把函数切成了两段:

  • await 之前:在当前状态执行
  • await 之后:挂到 Promise 的微任务回调里,等待下一次 resume

二、await 不是”暂停主线程”

理解这一点对于真正搞清楚 async/await 的行为很关键:

async function test() {
  console.log('Before await');
  await Promise.resolve();
  console.log('After await'); // 这行在微任务里执行
}

test();
console.log('After test()');

// 输出:
// Before await
// After test()    ← 不是 "After await"!
// After await

执行过程:

  1. test() 被调用,执行到 await,状态机切到 state 1,把 After await 那段代码挂到 Promise.resolve()then 回调(微任务)里
  2. test() “返回”(实际上返回了它的内部 Promise),主线程继续
  3. console.log('After test()') 执行
  4. 当前宏任务(同步代码)结束,微任务队列清空
  5. After await 的微任务执行

“暂停”只是状态机的挂起,主线程从未停止。 这是 async/await 和真正的协程(如 Go goroutine)的根本区别——V8 的实现是单线程的,没有真正的并行,只有通过事件循环的异步调度。

三、V8 的字节码层面

V8 为 await 生成专用字节码指令 Await。执行到 Await 时:

  1. V8 创建 PromiseReaction 对象,存储当前执行位置(Continuation)和上下文
  2. 将 Continuation 打包成微任务,注册到等待的 Promise 上
  3. 当前函数的栈帧被”挂起”(实际上是弹出调用栈,但状态保存在闭包里)
  4. 主线程继续执行后续代码

Promise 解析后,V8 调用 ResumeGenerator 字节码:

  1. PromiseReaction 里取出 Continuation
  2. 恢复上下文(变量状态、执行位置)
  3. await 的下一行继续执行

四、多个 await 的执行顺序

async function multiAwait() {
  console.log('1');
  await step1(); // 微任务 1
  console.log('3');
  await step2(); // 微任务 2
  console.log('5');
}

multiAwait();
console.log('2');
// 输出:1 → 2 → 3(微任务 1 后)→ 4(step2 解析后微任务中)→ 5

每个 await 都是一次微任务调度点。如果 step1()step2() 相互独立,可以并行:

// 串行(效率低):总时间 = step1 + step2
async function serial() {
  const r1 = await step1(); // 等 step1 完成
  const r2 = await step2(); // 再等 step2 完成
  return [r1, r2];
}

// 并行(效率高):总时间 = max(step1, step2)
async function parallel() {
  const [r1, r2] = await Promise.all([step1(), step2()]);
  // 两个 Promise 同时开始,等两者都完成
  return [r1, r2];
}

这是 async/await 最常见的性能优化点:把没有依赖关系的 await 改为 Promise.all

五、错误处理的工作机制

async 函数的错误处理通过 Promise 的 reject 机制传递:

async function mightFail() {
  throw new Error('something went wrong');
  // 等价于:return Promise.reject(new Error("..."))
}

// 方式一:try-catch(同步风格)
async function handler() {
  try {
    await mightFail();
  } catch (e) {
    console.log('caught:', e.message);
  }
}

// 方式二:.catch()(Promise 风格)
mightFail().catch((e) => console.log('caught:', e.message));

状态机中,V8 在 switch 外面包了 try-catch(见第一节的等效代码),任何阶段抛出的错误都会被捕获并传递给外层 Promise 的 reject

六、async/await 对调用栈的影响

await 会让调用栈出现一个”断层”——await 之后的代码在新的微任务里执行,不在原来的调用栈帧里:

async function a() {
  await b();
  doSomething(); // 这行执行时,a() 的原始调用栈已经不在了
}

这影响错误追踪:抛出错误时,调用栈里可能看不到完整的调用链。Chrome DevTools 提供了 “Async Call Stack” 功能(在 Sources 面板启用),可以重建跨 await 的完整调用链,便于调试。

# Node.js 中查看异步调用栈
node --async-stack-traces your-script.js

七、总结

async/await 在 V8 内部的实现是 Promise + 状态机:

  • async 函数被编译成一个返回 Promise 的函数,内部维护一个状态机
  • 每个 await 是一个状态切换点,把后续代码挂成微任务,主线程继续执行
  • await 的”暂停” 是状态机挂起,不是主线程阻塞;恢复通过微任务队列触发

理解这套机制,能清楚解释:为什么 await 后的代码比紧随其后的同步代码晚执行、为什么串行 awaitPromise.all 慢、为什么调试 async 函数时调用栈有断层。


本系列其他文章:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;