基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

JavaScript async/await 的底层实现原理

知识点图片

JavaScript 中的 async/await 被称为异步编程的“终极解决方案”,它让异步代码看起来像同步代码。

要理解它的底层实现原理,可以用一句话概括:async/await 本质上是 Generator(生成器)和 Promise 的语法糖,并内置了自动执行器。

下面我们分步骤深度剖析其底层原理。


一、 核心基础:Generator 函数(协程)

async/await 能够“暂停”和“恢复”代码执行,其背后的功臣是 Generator(生成器)。Generator 是基于协程(Coroutine)实现的。

  • 协程是一种比线程更轻量级的存在,一个线程上可以存在多个协程,但同时只能执行一个。
  • Generator 函数通过 yield 关键字交出执行权(暂停),通过 .next() 方法恢复执行权。
javascript
function* myGenerator() {
    console.log("Step 1");
    const res1 = yield 'A'; // 遇到 yield 暂停,把 'A' 传出去
    console.log("Step 2 received:", res1);
    const res2 = yield 'B';
    return 'C';
}

const gen = myGenerator();
console.log(gen.next());      // 执行到第一个 yield 暂停。打印 "Step 1", 返回 { value: 'A', done: false }
console.log(gen.next(100));   // 恢复执行,把 100 传给 res1。打印 "Step 2 received: 100", 返回 { value: 'B', done: false }
console.log(gen.next());      // 恢复执行,返回 { value: 'C', done: true }

局限性:Generator 虽然能暂停代码,但它需要我们在外部手动调用 .next() 才能继续往下走,这对于复杂的异步流程来说非常麻烦。


二、 引入 Promise 包装异步操作

如果让 yield 后面跟的是一个 Promise,我们就可以利用 Promise 的 then 方法,在异步操作成功后,再调用 .next() 恢复执行。

javascript
function request(data) {
    return new Promise(resolve => setTimeout(() => resolve(data), 1000));
}

function* asyncFlow() {
    const res1 = yield request('数据1');
    console.log(res1);
    const res2 = yield request('数据2');
    console.log(res2);
}

// 手动执行
const gen = asyncFlow();
gen.next().value.then(res1 => {
    gen.next(res1).value.then(res2 => {
        gen.next(res2);
    })
});

这就是 async/await 的雏形,但手动写 .then() 嵌套依然很丑陋(回调地狱)。


三、 核心灵魂:自动执行器(类似于 co 模块)

async/await 的神奇之处在于:它自带了自动执行器。它能自动判断 yield 出来的 Promise 状态,当 Promise resolve 后,自动把结果传回 Generator 并调用 .next()

我们可以手写一个 spawn 函数来模拟这个底层过程。这也是 Babel 编译 async/await 的核心逻辑(转换成 ES5 的代码):

javascript
// 模拟 async/await 的底层自动执行器
function spawn(genF) {
    // async 函数执行后返回的是一个 Promise
    return new Promise(function(resolve, reject) {
        const gen = genF(); // 实例化 Generator

        // 内部定义一个自动执行 next 的递归函数
        function step(nextF) {
            let next;
            try {
                // 执行 nextF(),实际上就是执行 gen.next()
                next = nextF(); 
            } catch(e) {
                // 如果内部报错,直接 reject
                return reject(e); 
            }

            // 如果 Generator 执行完毕了,直接 resolve 返回值
            if(next.done) {
                return resolve(next.value);
            }

            // 如果没执行完,将 yield 后面的值统一包装成 Promise
            // Promise 完成后,递归调用 step,把结果塞回 Generator
            Promise.resolve(next.value).then(
                function(v) { step(function() { return gen.next(v); }); },
                function(e) { step(function() { return gen.throw(e); }); }
            );
        }

        // 首次启动 Generator
        step(function() { return gen.next(undefined); });
    });
}

现在,我们用上面的 spawn 函数包装一下之前的 Generator:

javascript
// 你写的代码:
// async function test() {
//    const res1 = await request('数据1');
//    const res2 = await request('数据2');
//    return res2;
// }

// 引擎底层的等价代码:
const test = spawn(function* () {
    const res1 = yield request('数据1');
    console.log(res1);
    const res2 = yield request('数据2');
    console.log(res2);
    return res2;
});

四、 从 V8 引擎/微任务队列的角度看

上述 Generator 的解释是语法降级(Polyfill/Babel)的原理。但在现代浏览器的 V8 引擎原生实现中,并没有真的去创建一个 Generator,而是直接利用了微任务队列(Microtask Queue)协程挂起/恢复(Suspend/Resume)机制。

当 V8 引擎执行到一个 async 函数,遇到 await 时:

  1. 计算表达式: 引擎首先执行 await 右侧的表达式。
  2. 构建 Promise: 将右侧表达式的结果用 Promise.resolve() 包装起来。
  3. 挂起上下文: 核心! 引擎会保存当前 async 函数的执行上下文(局部变量、调用栈位置等),然后暂停该函数的执行,将控制权交还给外部的同步代码。
  4. 推入微任务: 引擎默默地为刚才那个 Promise 注册了一个 .then() 回调(实际上是一个微任务)。这个回调的作用是:当 Promise 决议后,恢复刚才保存的执行上下文,把结果赋值给 await 左侧的变量,并继续执行下一行代码。

一句话总结 V8 的微任务机制:
await 下面的代码,实际上都被丢进了 await 后面那个 Promise 的 .then() 回调里(即微任务队列中)。


总结

  • 表象: 同步写法的异步代码。
  • 语法本质: Generator 函数 + Promise + 自动执行器
  • 引擎底层(V8): 利用协程暂停/恢复当前执行上下文,并将 await 之后的代码作为微任务推入 Event Loop。
00:00
00:00