1. 前言
相信在实际开发中,Promise大家一定用了不少,async/await也一定用过,但是大家对于它的认识大部分只停留在:用同步写异步?语法糖?
大厂面试越来越偏向问你原理,一个你觉得很简单的知识,一问原理可能就会回答的支支吾吾,所以本文将从大家最常见的async/await,来和大家好好聊聊它的原理。
2. async/await的实际使用例子
在真正说原理之前,我们还是需要带大家好好回顾一下async/await的使用:
场景:当我们有一个用于从服务器获取用户数据的异步函数,我需要先获取数据在进行处理,之后在输出结果,代码应该如何写呢?
Promise版本:
function showUserInfo(userId) {
return fetchUserData(userId)
.then(function(user) {
const info = `用户:${user.name},年龄:${user.age}`;
console.log(info);
return info;
})
.catch(function(err) {
console.error("获取用户信息失败:", err);
throw err;
});
}
async/await版本:
// 一个模拟异步请求的函数,返回 Promise
function fetchUserData(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: "张三", age: 25 });
}, 1000);
});
}
// 实际使用 async/await
async function showUserInfo(userId) {
try {
// 等待异步请求完成
const user = await fetchUserData(userId);
// 处理数据
const info = `用户:${user.name},年龄:${user.age}`;
// 输出结果
console.log(info);
return info;
} catch (err) {
// 错误处理
console.error("获取用户信息失败:", err);
throw err;
}
}
// 调用
showUserInfo(1001);
最后输出:用户:张三,年龄:25
我们通过这个小小的例子就可以发现,比起使用promise这种方式在函数里写异步方法,我们使用async/await这种方式将异步方法同步化。增加代码的可读性,整体看着也更加美观
3. async/await 的底层原理
关于babel语法降级的错误认知
首先大家要明确一个概念,不要将语法降级当作真正的原理。 现在大部分文章都是讲Babel如何将async/await转换成generator + Promise的兼容代码。将这一串兼容代码当作其真正的原理。实际上这是错误的认知。
我们可以先来看看使用generator + Promise的兼容代码是如何实现async/await的语法降级兼容低版本浏览器的
Generator函数的使用
什么是Generator
Generator(生成器)是ES6引入的一种可以中断和恢复执行的函数。
语法:
用functon*定义
内部用yield关键字“暂停函数的执行”
function* gen() {
yield 1;
yield 2;
yield 3;
}
- fuction* 声明生成器函数
- yield 用于暂停函数的执行,并在后续语句产出值
const g = gen(); // g 是一个生成器对象
在使用时我们需要定义gen()创建生成器对象
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: false }
console.log(g.next()); // { value: undefined, done: true }
我们通过next()方法,每次调用其,函数就会从上一个yield处恢复执行直到下一个yield或函数结束。
value是产出的值,done表示是否执行完毕
yield的用处:
- yiled可以暂停函数的执行,并返回一个值
- 下次调用next()时,可以从暂停处继续往下执行
我们的next()方法也能够传参
-
第一次调用 next() 时,传参无效(会被忽略),因为此时 Generator 还没开始执行,参数无法传递到第一个 yield。
-
从第二次 next(value) 开始,传入的参数会作为上一个 yield 表达式的返回值,赋值给左侧变量。
function* gen() {
const a = yield 1;
console.log('a:', a);
const b = yield 2;
console.log('b:', b);
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next('foo')); // a: foo { value: 2, done: false }
console.log(g.next('bar')); // b: bar { value: undefined, done: true }
- 第一次 next(),yield 1,暂停,返回 1
- 第二次 next('foo'),'foo' 作为上一个 yield 的返回值,赋给 a
- 第三次 next('bar'),'bar' 作为上一个 yield 的返回值,赋给 b
我们讲完了Generator的基本用法后,我们如何使用Generator + Promise将async进行语法降级呢?
async/await 降级为 Generator 时的流程
Babel 等工具在将 async/await 降级为 generator + Promise 时,会用到 next 传参机制:
- 每次 yield 一个 Promise,外部用 then 拿到结果后,通过 next(result) 把结果传回 generator 内部,赋值给 await 左侧的变量。
- 这样 generator 内部的变量赋值、流程控制就和 async/await 的语义一致了。
这里写伪代码示例
假设这里有async代码;
async function foo() {
const a = await step1();
const b = await step2(a);
return b;
}
降级为generator:
function* foo() {
const a = yield step1();
const b = yield step2(a);
return b;
}
降级之后需要使用promise进行自动化调用:
const gen = foo();
gen.next().value.then(res1 => {
gen.next(res1).value.then(res2 => {
gen.next(res2);
});
});
- 第一次 gen.next(),yield step1(),外部 then 拿到结果 res1
- 第二次 gen.next(res1),res1 作为 await step1() 的结果,赋值给 a
- 依此类推
当然这样写需要手动 next、手动 then,嵌套多了很难维护。所以我们就封装一个高阶函数,自动驱动generator,返回一个promise:
function autoRun(generatorFn) {
return function(...args) {
return new Promise((resolve, reject) => {
const gen = generatorFn(...args);
function step(nextF, arg) {
let next;
try {
next = nextF.call(gen, arg);
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
// next.value 应该是 Promise
Promise.resolve(next.value).then(
v => step(gen.next, v),
e => step(gen.throw, e)
);
}
step(gen.next);
});
};
}
当我们的generator函数为
function* foo() {
const res1 = yield p(1);
const res2 = yield p(res1);
return res2;
}
我们就可以这样进行调用
const asyncFoo = autoRun(foo);
asyncFoo().then(result => {
console.log('最终结果:', result);
});
通过上面的一些讲解,可能会给大家产生一些错觉:这个好像就是原理啊。都拆的这么细了。其实不然。
Generator + Promise + 自动驱动,只是 async/await 的“兼容实现”或“语法降级方案”,并不是 async/await 的底层原理。
这些方案的本质,是为了让不支持 async/await 的老环境也能用上类似的语法和功能。它们通过 Generator 的暂停-恢复机制,模拟了 async/await 的行为,但这只是“表象”。
真正的 async/await 原理,其实藏在 JavaScript 引擎(如 V8)的底层实现中。
- async/await 在引擎内部会被编译成状态机,每个 await 都是一个 “暂停点”。
- 遇到 await 时,执行帧(变量、状态、作用域等)会被保存到堆上,Promise 完成后再恢复执行。
- 整个过程由引擎自动完成,性能和调试体验都远超语法降级方案。
那么,async/await 的底层原理到底是什么?我们接下来就走进 V8 引擎,看看 async/await 在底层是如何被实现的。
async/await在V8引擎的底层原理:
状态机原理
async/await 在引擎内部会被编译成状态机,每个 await 都是一个“暂停点”
- 当你写下一个async函数后,v8会在编译阶段把它拆解成多个“状态”
- 每遇到一个await,就会生成一个 “暂停点”,把函数分割成多个状态
- 这些状态的切换由Promise的完成(resolve/reject)来驱动
伪代码示意:
async function foo() {
const a = await step1();
const b = await step2(a);
return b;
}
V8 会把它拆成类似这样的状态机:
switch (state) {
case 0:
promise1 = step1();
state = 1;
return promise1.then(res => {
a = res;
runStateMachine();// 这里恢复状态机,继续执行 case 1
});
case 1:
promise2 = step2(a);
state = 2;
return promise2.then(res => {
b = res;
runStateMachine();
});
case 2:
return b;//这里恢复状态机,继续执行 case 2
}
runStateMachine 的含义
- 本质上,它代表“恢复 async 函数的执行”。
- 当某个 await 后面的 Promise 完成时,状态机会切换到下一个状态,继续执行后续代码。
伪代码的其他部分:
- 每个 await 都是一个状态的分界点。
- 状态机会自动切换,直到所有 await 执行完毕。
- 每次 Promise 完成后,runStateMachine() 就会让状态机跳到下一个 case,继续执行 async 函数剩下的逻辑。
关于执行帧的保存与恢复
执行帧: async函数暂停时的快照,表示当前暂停的状态下,所有的局部变量,作用域链,异常处理信息等。
当函数的执行遇到async后,执行帧会被保存到堆上,Promise完成后再恢复执行。
为什么在堆上?
因为 await 之后的代码会在未来的某个时刻(Promise 完成后)才恢复执行,这时原来的调用栈早已清空,只有堆上的快照能保证上下文不丢失。
保存与恢复流程:
1.执行到 await,V8 把当前执行帧序列化,存到堆上。
2.Promise 完成后,V8 从堆上反序列化出执行帧,恢复所有变量和状态,继续执行下一个状态。
3.这样即使 async 函数多次暂停和恢复,所有上下文都不会丢失。
举个小🌰
async function foo() {
let a = 1;
await bar();
let b = 2;
await baz();
return a + b;
}
- 第一次遇到 await bar(),保存 a=1、状态=1 到堆上。
- bar() 完成后,恢复帧,执行 let b=2,再遇到 await baz(),保存 a=1、b=2、状态=2 到堆上。
- baz() 完成后,恢复帧,执行 return a+b。
微任务队列调度
await后续代码执行时机: await后续代码不会很快执行,而是被引擎打包成一个微任务,放入微任务队列,再经历一次事件循环,才能够执行。
异常处理
如果 await 的 Promise 被 reject,异常会被抛回到 async 函数暂停点,V8 会查找最近的 catch 块,跳转到 catch 代码块继续执行。
如果没有 catch,async 函数返回的 Promise 会变为 rejected,外部可以用 .catch 捕获。
图解一手总体流程:
4.总结
1.我们在学习原理时,不应该是为了兼容到低版本浏览器而语法降级的代码视作原理。比如async是10。使用Generator + promise这个方法就相当于 2 + 8。诚然2+8确实是10,Generator + promise也确实能实现,但不能说2 + 8就是原理。所以我们在分析原理时就需要从引擎角度去看待问题。
2.async/await 在引擎内部会被编译成状态机,每个 await 都是一个暂停点,执行帧会被保存到堆上,Promise 完成后再恢复执行,整个过程由引擎自动调度和优化,性能和调试体验都远超语法降级方案。
参考资料: