好的,我们来彻底讲明白 async/await 的底层原理。
一句话概括:async/await 是基于 Promise 和 Generator 函数实现的语法糖,它通过事件循环(Event Loop)机制,让我们能用看似同步的代码风格来处理异步操作,从而避免了回调地狱和冗长的 .then() 链。
为了彻底理解,我们需要从根源上拆解这个过程,分为以下几个核心部分:
-
前置知识:异步的进化之路
-
核心基石1:
Promise- 异步操作的容器 -
核心基石2:
Generator- 函数执行的暂停与恢复 -
async/await的实现:Generator+ 自动执行器 -
引擎的底层协作:事件循环(Event Loop)与微任务(Microtask)
-
总结
1. 前置知识:异步的进化之路
要理解 async/await 为何出现,就要看它解决了什么问题。
-
Callback(回调函数)时代:最早的异步处理方式。当一个异步操作完成时,调用我们预先传入的函数。这很容易导致“回调地狱”(Callback Hell),代码横向发展,难以阅读和维护。
JavaScript
step1(function (value1) { step2(value1, function (value2) { step3(value2, function (value3) { // ... and so on }); }); }); -
Promise时代:Promise对象代表一个异步操作的最终完成(或失败)及其结果值。它将回调函数变成了链式调用.then(),将代码从横向拉回了纵向,解决了回调地狱问题,但代码依然有些冗余。JavaScript
step1() .then(value1 => step2(value1)) .then(value2 => step3(value2)) .catch(error => console.error(error));
async/await 的目标就是让上面 Promise 的代码写法变得更像我们习惯的同步代码。
2. 核心基石1:Promise - 异步操作的容器
async/await 是建立在 Promise 之上的,这是理解它的第一个关键点。
-
async函数的返回值:一个async函数无论内部执行了什么,其返回值永远是一个Promise对象。-
如果函数内部
return了一个非Promise的值(如return 10;),这个值会被自动包装成一个状态为fulfilled的Promise,其值为10。 -
如果函数内部
return了一个Promise,那么该async函数的返回值就是这个Promise。 -
如果函数内部抛出异常,则相当于返回一个状态为
rejected的Promise。
-
-
await操作的对象:await后面通常跟的是一个Promise对象。它会“等待”这个Promise的状态变为fulfilled,然后返回其结果;或者等待其变为rejected,然后将拒绝的原因作为异常抛出。如果await后面跟的不是Promise,它会立即将其转换为一个已解决的Promise。
所以,Promise 是 async/await 进行异步流程控制的“数据载体”和“状态机”。
3. 核心基石2:Generator - 函数执行的暂停与恢复
这是揭示 async/await 底层魔法的最核心部分。Generator 函数是 ES6 引入的一种特殊的函数,它可以在执行过程中暂停和恢复。
看一个 Generator 函数的例子:
JavaScript
function* myGenerator() {
console.log('第一段');
const value1 = yield 1; // 暂停点1
console.log('从暂停点1恢复,接收到的值:', value1);
console.log('第二段');
const value2 = yield 2; // 暂停点2
console.log('从暂停点2恢复,接收到的值:', value2);
return '执行完毕';
}
// 调用 Generator 函数并不会立即执行,而是返回一个迭代器对象
const gen = myGenerator();
// 手动控制执行
console.log(gen.next()); // 输出 "第一段", 然后在 yield 1 处暂停。返回 { value: 1, done: false }
console.log(gen.next('你好')); // 从暂停点1恢复,将'你好'作为 yield 1 的返回值赋给 value1。然后在 yield 2 处暂停。返回 { value: 2, done: false }
console.log(gen.next('世界')); // 从暂停点2恢复,将'世界'作为 yield 2 的返回值赋给 value2。函数执行完毕。返回 { value: '执行完毕', done: true }
关键点:
-
function*定义了一个Generator函数。 -
yield关键字是暂停点,它会产出一个值,并暂停函数的执行。 -
gen.next()方法用于从暂停点恢复执行。next()的参数会成为上一个yield表达式的返回值。
async/await 正是利用了 Generator 的这个“暂停/恢复”能力,实现了对异步代码的“挂起”和“继续”。
4. async/await 的实现:Generator + 自动执行器
async/await 本质上可以看作是一个自动执行的 Generator 函数。我们不需要手动调用 .next(),一个“执行器”(Runner)会帮我们自动完成。
让我们来模拟一下这个过程。假设我们有这样的 async 函数:
JavaScript
// 这是我们的目标 async 函数
async function fetchData() {
console.log('开始请求数据...');
const result1 = await request('api/data1'); // 假设 request 返回一个Promise
console.log('拿到数据1:', result1);
const result2 = await request('api/data2');
console.log('拿到数据2:', result2);
return '全部完成';
}
上面的代码,在底层可以被理解为(这正是 Babel 等转译器所做的事情):
第一步:Generator 化
JavaScript
function* fetchDataGenerator() {
console.log('开始请求数据...');
const result1 = yield request('api/data1'); // yield 后面是一个 Promise
console.log('拿到数据1:', result1);
const result2 = yield request('api/data2'); // yield 后面是另一个 Promise
console.log('拿到数据2:', result2);
return '全部完成';
}
第二步:自动执行器(Runner)
我们需要一个函数来自动执行这个 Generator。
JavaScript
function run(genFunc) {
const gen = genFunc(); // 得到迭代器
function next(data) {
const result = gen.next(data); // 执行下一步,并传入上次Promise的结果
if (result.done) {
// 如果 Generator 执行完毕,就将最终结果 resolve 出去
return Promise.resolve(result.value);
}
// 如果还没完,result.value 是一个 Promise
// 我们等待这个 Promise 完成,然后用它的结果递归调用 next
return Promise.resolve(result.value).then(
resolvedData => next(resolvedData), // 成功时,将结果传给下一次 next
error => gen.throw(error) // 失败时,将错误抛入 Generator
);
}
return next(); // 启动执行器
}
// 使用自动执行器
run(fetchDataGenerator).then(finalResult => {
console.log(finalResult); // 输出 "全部完成"
});
这个 run 函数就是 async/await 的核心原理:
-
启动
Generator。 -
遇到
yield(相当于await),它后面跟的是一个Promise。 -
run函数拿到这个Promise,然后注册.then()回调。 -
在
.then()回调中,当Promise完成后,将其结果作为参数,调用gen.next(),将结果注入Generator函数内部,并使其从上次暂停的位置继续执行。 -
这个过程循环往复,直到
Generator执行完毕 (result.done为true)。
所以,await 关键字的作用就是 yield 一个 Promise 并把控制权交给了这个自动执行器。
5. 引擎的底层协作:事件循环(Event Loop)与微任务(Microtask)
现在我们知道函数是如何“暂停”和“恢复”的了,但还有一个关键问题:在 await 等待 Promise 结果的期间,JavaScript 引擎在做什么?为什么它没有阻塞整个程序的运行?
答案是事件循环(Event Loop)和微任务队列(Microtask Queue)。
执行 async 函数的完整流程如下:
-
进入
async函数:fetchData()函数被调用,它的代码开始同步执行。 -
遇到
await:执行到await request('api/data1')。-
request('api/data1')被立即执行,它发起一个网络请求(这是一个异步的 Web API),并返回一个Promise对象。 -
await关键字会“暂停”fetchData函数的执行。重要的是,它只是暂停了fetchData函数本身,并没有阻塞 JavaScript 的主线程。fetchData函数的执行上下文(Execution Context)会暂时被移出调用栈(Call Stack)。 -
JavaScript 引擎会继续执行
fetchData函数之后的同步代码(如果fetchData是被另一个函数调用的)。
-
-
Promise 状态改变:一段时间后,网络请求完成,
request返回的那个Promise的状态从pending变为fulfilled(或rejected)。 -
注册微任务:当
Promise状态改变时,它的.then()回调(也就是我们模拟的run函数中用于调用next()的那部分逻辑)会被放入微任务队列(Microtask Queue)。 -
事件循环检查:事件循环在当前宏任务(Macrotask)执行完毕,且调用栈为空时,会立即检查微任务队列。
-
恢复执行:事件循环从微任务队列中取出回调函数并执行。这个回调函数会调用
gen.next(),将Promise的结果传入,并将fetchData函数的执行上下文重新推入调用栈,从上次await的地方继续执行,直到遇到下一个await或函数结束。
这个机制确保了在等待异步操作(如 I/O、网络请求)时,主线程可以继续处理其他任务(如用户交互、渲染等),从而实现了非阻塞的异步执行。
6. 总结
让我们把所有知识点串起来,形成一个完整的图像:
-
async关键字:声明一个函数是异步的,它的返回值必然是一个Promise。 -
await关键字:只能在async函数内部使用。它实际上是Generator的yield和Promise的.then()的组合。 -
Generator函数:是async/await实现函数“暂停”和“恢复”的底层机制。async函数就是一个特殊的Generator函数。 -
自动执行器:
async/await内置了一个Generator的自动执行器,它负责在Promise完成后自动调用.next(),将异步流程串联起来。 -
Promise对象:是async/await异步流程中传递状态和数据的载体。 -
事件循环与微任务:是
async/await实现非阻塞等待的底层保障。await暂停函数执行后,会将恢复操作作为微任务放入队列,等待主线程空闲时执行。
最终,async/await 就是 JavaScript 引擎为我们精心打造的一个高级抽象。它巧妙地将 Generator 的执行控制能力和 Promise 的状态管理能力结合在一起,再通过事件循环机制进行调度,最终呈现给我们一种极其简洁、易于理解和维护的同步化编程体验。