深入解析 JavaScript 引擎中 async/await 的实现原理

作者:代码拾荒时间:2026-10-25 08:22:01浏览:575

本文深入剖析 JavaScript 引擎实现 async/await 的底层机制。从生成器(Generator)与协程(Coroutine)的工作方式入手,解析函数暂停与恢复的原理,并结合 Promise 微任务队列,详细拆解 async/await 如何将异步代码转化为同步风格执行,揭示其背后的引擎实现细节。

前言

Promise 的引入有效解决了回调地狱问题,但其链式调用中大量的 then() 方法使得复杂流程的代码可读性降低,语义表达不够直观。尽管请求流程已线性化,繁琐的 then 链仍增加了阅读难度。为此,ES7 引入了 async/await,这是 JavaScript 异步编程的重要演进。它允许开发者在不阻塞主线程的前提下,以同步代码的形式访问异步资源,显著提升了代码逻辑的清晰度。

本文旨在从底层技术视角,逐步解析 JavaScript 引擎如何实现 async/await。直接介绍用法可能令人困惑,因此我们将深入其核心技术点,厘清 async 和 await 的工作机制。

我们将首先探讨生成器(Generator)的工作原理及其底层实现——协程(Coroutine)。由于 async/await 依赖于 Generator 和 Promise 两种技术,我们将结合这两者,分析 async/await 如何实现以同步方式编写异步代码。

生成器 VS 协程

生成器函数是一种带有星号(*)的函数,具备暂停执行和恢复执行的能力。

function* genDemo() {
    console.log("开始执行第一段")
    yield 'generator 2'

    console.log("开始执行第二段")
    yield 'generator 2'

    console.log("开始执行第三段")
    yield 'generator 2'

    console.log("执行结束")
    return 'generator 2'
}

console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

运行上述代码并观察输出,会发现 genDemo 函数并非一次性执行完毕,而是全局代码与 genDemo 函数交替执行。这正是生成器函数的特性:支持暂停与恢复。接下来我们将具体展示生成器函数的使用方式。

  • 在生成器函数内部,若遇到 yield 关键字,JavaScript 引擎会返回该关键字后的值给外部调用者,并暂停当前函数的执行。
  • 外部调用者可以通过 next 方法恢复函数的执行。

对于函数的暂停与恢复机制,读者可能好奇其底层原理。下文将简要介绍 V8 引擎如何实现函数的暂停与恢复,这有助于理解后续的 async/await 机制。

理解函数暂停与恢复的关键在于协程(Coroutine)的概念。协程是一种比线程更轻量级的执行单元,可视为运行在线程上的任务。一个线程中可以存在多个协程,但在同一时刻只能执行一个协程。例如,当执行 A 协程并需要启动 B 协程时,A 必须将主线程的控制权移交给 B,表现为 A 暂停而 B 恢复;反之亦然。通常,若从 A 启动 B,则称 A 为 B 的父协程。

正如进程包含多个线程,线程也可包含多个协程。关键在于,协程不由操作系统内核管理,而是在用户态由程序完全控制。这种机制大幅提升了性能,避免了线程切换带来的资源消耗。

为帮助理解协程的执行过程,结合前述代码执行流程绘制了“协程执行流程图”,请对照代码进行分析。

前言

从流程图中可总结出协程的四条规则:

  • 调用生成器函数 genDemo 会创建协程 gen,但创建后 gen 并不会立即执行。
  • 需调用 gen.next 来启动 gen 协程的执行。
  • 协程执行过程中,若遇到 yield 关键字,将暂停 gen 协程,并将主要信息返回给父协程。
  • 若执行期间遇到 return 关键字,JavaScript 引擎将终止当前协程,并将 return 后的值返回给父协程。

针对上述代码,可能存在疑问:父协程与 gen 协程各自拥有调用栈,当 gen 通过 yield 移交控制权给父协程时,V8 如何切换调用栈?父协程通过 gen.next 恢复 gen 时,又如何切换回 gen 的调用栈?

要解答上述问题,需关注以下两点。

第一,gen 协程与父协程在主线程上交替执行,而非并发执行。它们之间的切换通过 yield 和 gen.next 配合完成。

第二,当在 gen 协程中调用 yield 时,JavaScript 引擎会保存 gen 当前的调用栈信息,并恢复父协程的调用栈;反之,当父协程执行 gen.next 时,引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈。

为直观展示父协程与 gen 协程如何切换调用栈,

前言

至此,相信已清楚协程的工作机制。在 JavaScript 中,生成器即为协程的一种实现方式。接下来,我们将使用生成器和 Promise 改造开头的 Promise 代码,改造后的代码如下:

//foo函数
function* foo() {
    let response1 = yield fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
}

//执行foo函数的代码
let gen = foo()
function getGenPromise(gen) {
    return gen.next().value
}
getGenPromise(gen).then((response) => {
    console.log('response1')
    console.log(response)
    return getGenPromise(gen)
}).then((response) => {
    console.log('response2')
    console.log(response)
})

从图中可见,foo 函数为生成器函数,内部实现了以同步形式执行异步操作;但在 foo 外部,仍需编写执行 foo 的代码(如后半部分所示)。下文将分析该代码的工作机制。

  • 首先执行 let gen = foo(),创建 gen 协程。随后在父协程中通过 gen.next 将主线程控制权移交给 gen 协程。
  • gen 协程获取控制权后,调用 fetch 函数创建 Promise 对象 response1,并通过 yield 暂停 gen 协程,将 response1 返回给父协程。
  • 父协程恢复执行后,调用 response1.then 方法等待请求结果。
  • 当 fetch 发起的请求完成后,then 中的回调函数被调用。回调函数获取结果后,通过 gen.next 放弃主线程控制权,将控制权交还 gen 协程以继续执行下一个请求。

以上即为协程与 Promise 配合执行的大致流程。通常,我们会将执行生成器的代码封装为一个函数,称为执行器(Executor),可参考著名的 co 框架,如下所示:

function* foo() {
    let response1 = yield fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
}
co(foo());

通过生成器配合执行器,即可实现以同步方式编写异步代码,显著提升代码可读性。

async/await

尽管生成器已能满足需求,但 ES7 引入了 async/await,旨在彻底摆脱执行器和生成器,实现更直观简洁的代码。async/await 的本质是 Promise 和生成器的应用,底层涉及微任务和协程。为弄清其原理,需分别分析 async 和 await。

async

首先明确 async 的定义。根据 MDN,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。

下面通过代码展示如何隐式返回 Promise:

async function foo() {
    return 2
}
console.log(foo())  // Promise {<resolved>: 2}

运行上述代码,调用由 async 声明的 foo 函数会返回一个状态为 resolved 的 Promise 对象,结果如下:

Promise {<resolved>: 2}

await

已知 async 函数返回 Promise 对象,结合文中代码分析 await 的含义。

async function foo() {
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)
}
console.log(0)
foo()
console.log(3)

观察上述代码,能否判断打印内容?这需分析 async 结合 await 的执行过程。在详细解释前,先从协程视角查看整体执行流程图:

前言

结合上图,分析 async/await 的执行流程。

首先执行 console.log(0),打印输出 0。

接着执行 foo 函数。由于 foo 被 async 标记,进入该函数时,JavaScript 引擎会保存当前调用栈等信息,然后执行 foo 中的 console.log(1),打印输出 1。

随后执行 foo 中的 await 100 语句,这是分析重点。执行 await 100 时,JavaScript 引擎在后台执行了大量操作。下文将拆解该语句,分析引擎的具体行为。

执行到 await 100 时,默认创建一个 Promise 对象,代码如下:

let promise_ = new Promise((resolve,reject){
  resolve(100)
})

在 promise_ 对象创建过程中,executor 函数调用了 resolve 函数,JavaScript 引擎会将该任务提交至微任务队列。

随后,JavaScript 引擎暂停当前协程的执行,将主线程控制权交还给父协程,同时将 promise_ 对象返回给父协程。

主线程控制权已移交父协程,此时父协程需调用 promise_.then 来监控 promise 状态的变化。继续执行父协程流程,执行 console.log(3),打印输出 3。

随后父协程执行结束,在结束前进入微任务检查点,执行微任务队列。队列中包含 resolve(100) 任务,执行该任务时,会触发 promise_.then 中的回调函数,如下所示:

promise_.then((value)=>{
   //回调函数被激活后
  //将主线程控制权交给foo协程,并将vaule值传给协程
})

该回调函数被激活后,将主线程控制权交还给 foo 函数的协程,并将 value 值传递给该协程。

foo 协程被激活后,将 value 值赋给变量 a,随后继续执行后续语句。执行完成后,将控制权归还给父协程。

以上即为 await/async 的执行流程。正是由于 async 和 await 在后台执行了大量工作,我们才能以同步方式编写异步代码。

小结

Promise 编程模型虽解决了回调地狱,但仍充斥大量 then 方法,语义化不足。async/await 的引入正是为了解决这一问题。

使用 async/await 可实现以同步代码风格编写异步代码,其基础技术包括生成器和 Promise。生成器作为协程的实现,利用其暂停和恢复特性,支撑了异步流程的控制。

此外,V8 引擎为 async/await 提供了大量语法层面的包装。了解背后的代码实现有助于加深理解。async/await 无疑是异步编程领域的重大革新,也是未来的主流编程风格。

除 JavaScript 外,Python、Dart、C# 等语言也引入了 async/await。它不仅使代码更整洁美观,还确保函数始终返回 Promise。

总结

本文介绍了 JavaScript 引擎实现 async/await 的原理。更多相关 js 实现 async/await 内容,请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

作者最新文章