JavaScript async/await 的底层实现原理
JavaScript 中的 async/await 被称为异步编程的“终极解决方案”,它让异步代码看起来像同步代码。
要理解它的底层实现原理,可以用一句话概括:async/await 本质上是 Generator(生成器)和 Promise 的语法糖,并内置了自动执行器。
下面我们分步骤深度剖析其底层原理。
一、 核心基础:Generator 函数(协程)
async/await 能够“暂停”和“恢复”代码执行,其背后的功臣是 Generator(生成器)。Generator 是基于协程(Coroutine)实现的。
- 协程是一种比线程更轻量级的存在,一个线程上可以存在多个协程,但同时只能执行一个。
- Generator 函数通过
yield关键字交出执行权(暂停),通过.next()方法恢复执行权。
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() 恢复执行。
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 的代码):
// 模拟 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:
// 你写的代码:
// 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 时:
- 计算表达式: 引擎首先执行
await右侧的表达式。 - 构建 Promise: 将右侧表达式的结果用
Promise.resolve()包装起来。 - 挂起上下文: 核心! 引擎会保存当前
async函数的执行上下文(局部变量、调用栈位置等),然后暂停该函数的执行,将控制权交还给外部的同步代码。 - 推入微任务: 引擎默默地为刚才那个 Promise 注册了一个
.then()回调(实际上是一个微任务)。这个回调的作用是:当 Promise 决议后,恢复刚才保存的执行上下文,把结果赋值给await左侧的变量,并继续执行下一行代码。
一句话总结 V8 的微任务机制:await 下面的代码,实际上都被丢进了 await 后面那个 Promise 的 .then() 回调里(即微任务队列中)。
总结
- 表象: 同步写法的异步代码。
- 语法本质:
Generator函数 +Promise+ 自动执行器。 - 引擎底层(V8): 利用协程暂停/恢复当前执行上下文,并将
await之后的代码作为微任务推入 Event Loop。