async/await : 一场生成器和 Promise的里应外合
背景
说实话,async/await 我在日常工作中好像并没有很主动的去用,可能对于异步编程,我宁愿去用 Promise这种显式链式方式告诉自己,这是异步流程。同步式的代码让我有点心有余悸,为啥会这样放着更过“高级”的API不用呢?我想可能有一方面的原因是,我对它的了解不深吧,或者只停留在了解吧,这两天看了下 async/await 的原理,发现它并没有那么神秘,并且是站在两位巨人的肩膀上的。
巨人一号:生成器 Generator
Generator基本使用
Generator相信大家都不陌生啦,我们直接上代码看运行结果吧
// 定义一个生成器函数(普通函数 + *)
function* numberGenerator() {
console.log("生成器函数开始执行");
// 第一次yield,向外返回值
const firstValue = yield 1; // yield左边可以接收next传入的参数
console.log("第一个yield接收到的参数:", firstValue);
// 第二次yield
const secondValue = yield 2;
console.log("第二个yield接收到的参数:", secondValue);
// 第三次yield
yield 3;
console.log("生成器函数即将执行完毕");
// 函数结束,done会变为true
return "执行结束";
}
// 1. 调用生成器函数,不会执行函数体,只会得到生成器对象
const generator = numberGenerator();
console.log("调用生成器函数后得到的对象:", generator); // 输出 Generator 对象,无函数体执行日志
// 2. 第一次调用next(),函数体开始执行,直到第一个yield暂停
console.log("第一次next返回:", generator.next());
// 输出:
// 生成器函数开始执行
// 第一次next返回: { value: 1, done: false }
// 3. 第二次调用next()并传入参数,参数会作为上一个yield的返回值
console.log("第二次next返回:", generator.next("我是第一个yield的返回值"));
// 输出:
// 第一个yield接收到的参数: 我是第一个yield的返回值
// 第二次next返回: { value: 2, done: false }
// 4. 第三次调用next()并传入参数
console.log("第三次next返回:", generator.next("我是第二个yield的返回值"));
// 输出:
// 第二个yield接收到的参数: 我是第二个yield的返回值
// 生成器函数即将执行完毕
// 第三次next返回: { value: 3, done: false }
// 5. 第四次调用next(),函数执行到return,done变为true
console.log("第四次next返回:", generator.next());
// 输出:
// 第四次next返回: { value: '执行结束', done: true }
// 6. 后续调用next(),value为undefined,done保持true
console.log("第五次next返回:", generator.next());
// 输出:
// 第五次next返回: { value: undefined, done: true }
关键特性解释
-
生成器函数定义:
function* 函数名()是生成器函数的语法(*的位置可以在function后、函数名前,空格不影响),这是和普通函数最核心的区别。 -
调用不执行:调用
numberGenerator()不会执行函数体,只会返回一个Generator对象,这和普通函数调用立即执行完全不同。 -
yield 暂停与返回:
-
yield是生成器的核心关键词,执行到yield时,函数会暂停执行(而非结束),并将yield后的值作为next()返回对象的value。 -
yield不像return那样终止函数,下次调用next()会从暂停的位置继续执行。
-
-
next 传参:
generator.next(参数)传入的参数,会作为上一个yield语句的返回值(注意:第一个next()传参无效,因为此时还没有执行过任何yield)。 -
done 属性:
next()返回对象的done属性表示生成器是否执行完毕:-
false:生成器未执行完,还能继续调用next()获取值; -
true:生成器执行完毕(执行到return或函数末尾),后续next()的value为undefined(这一点我经常忽略)。
-
总结
- 生成器函数通过
function*定义,调用后返回生成器对象,而非立即执行函数体。 -
yield负责暂停函数并向外返回值,next()负责恢复执行,且next()可传参作为上一个yield的返回值。 -
next()返回的对象包含value(返回值)和done(执行状态),done: true表示生成器执行完毕。
Generator throw
// 定义一个包含异常处理的生成器函数
function* fooGenerator() {
console.log("fooGenerator 开始执行(第一次next触发)");
try {
// 第一个yield:暂停并返回值,后续可能接收next传参
const receivedValue = yield "第一个yield返回值";
console.log("第一个yield接收到next的参数:", receivedValue);
// 执行到这里时,如果外部调用throw(),异常会抛到当前执行位置
console.log("准备执行第二个yield...");
yield "第二个yield返回值";
console.log("生成器未被中断,继续执行");
} catch (error) {
// 捕获外部throw()抛出的异常
console.log("生成器内部捕获到异常:", error.message);
// 捕获异常后,生成器可继续yield返回值
yield "异常捕获后返回的兜底值";
}
console.log("生成器函数执行到末尾");
return "执行结束";
}
// 1. 调用生成器函数,仅得到生成器对象,函数体不执行
const generator = fooGenerator();
console.log("调用fooGenerator后得到的对象:", generator); // Generator 对象
// 2. 第一次调用next():函数体开始执行,直到第一个yield暂停
console.log("===== 第一次调用 next() =====");
const firstNext = generator.next();
console.log("第一次next返回:", firstNext);
// 输出:
// fooGenerator 开始执行(第一次next触发)
// 第一次next返回: { value: '第一个yield返回值', done: false }
// 3. 第二次调用next(参数):从第一个yield暂停处继续执行,参数作为yield返回值
console.log("\n===== 第二次调用 next('bar') =====");
const secondNext = generator.next("bar");
console.log("第二次next返回:", secondNext);
// 输出:
// 第一个yield接收到next的参数: bar
// 准备执行第二个yield...
// 第二次next返回: { value: '第二个yield返回值', done: false }
// 4. 调用throw():从第二个yield暂停处继续执行,但抛出异常
console.log("\n===== 调用 throw() 方法 =====");
const throwResult = generator.throw(new Error("外部手动抛出的异常"));
console.log("throw()返回:", throwResult);
// 输出:
// 生成器内部捕获到异常: 外部手动抛出的异常
// throw()返回: { value: '异常捕获后返回的兜底值', done: false }
// 5. 异常捕获后,生成器仍可继续执行next()
console.log("\n===== 异常后调用 next() =====");
const thirdNext = generator.next();
console.log("第三次next返回:", thirdNext);
// 输出:
// 生成器函数执行到末尾
// 第三次next返回: { value: '执行结束', done: true }
throw() 方法核心逻辑解释
-
throw() 的执行触发:
- 和
next()一样,调用generator.throw()会让生成器从上一次暂停的位置继续执行; - 但不同于
next()传入普通参数,throw()会在生成器当前执行位置主动抛出一个异常 -- 意思就是 从当前位置直接去走catch逻辑,try逻辑不会再走了。
- 和
-
异常的捕获与处理:
- 如果生成器内部有
try/catch块包裹了暂停位置后的执行逻辑,异常会被内部捕获,生成器不会立即终止; - 如果内部没有捕获,异常会抛出到外部,生成器状态变为
done: true,后续调用next()只会返回{ value: undefined, done: true }。
- 如果生成器内部有
-
throw() 的返回值:
- 即使抛出了异常,只要内部捕获了,
throw()也会返回和next()一样的对象({ value, done }); - 这个
value是异常捕获后,生成器继续执行到下一个yield时返回的值(示例中就是异常捕获后返回的兜底值)。
- 即使抛出了异常,只要内部捕获了,
总结
-
throw()方法和next()一样能触发生成器继续执行,但核心作用是向生成器内部抛出异常; - 生成器内部可通过
try/catch捕获throw()抛出的异常,捕获后生成器不会终止,仍可继续执行并返回新的yield值; - 生成器的执行始终是“暂停-恢复”模式:
next()恢复执行并传参,throw()恢复执行并抛异常,本质都是改变生成器的执行状态。
这个特性在异步编程中很实用,比如可以用 throw() 主动终止异步任务、处理异步流程中的错误等。
巨人二号:Promise
Promise大家应该再熟悉不过了,由于篇幅原因,这篇先不做深入了解
下面让我们看看他们结合在一起会发生什么吧
“回调式” 写法 =》 “同步式” 写法
// 传统 Promise 回调写法:有明显的回调嵌套感,代码“右移”
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(data => {
console.log('请求结果:', data);
// 如果还要发第二个请求,就要在这继续嵌套 then,越套越深
fetch(`https://jsonplaceholder.typicode.com/todos/${data.id + 1}`)
.then(res => res.json())
.then(data2 => console.log('第二个请求结果:', data2));
})
.catch(error => console.log('错误:', error));
以上代码,我们看到 虽然用到了Promise,但是也难免会出现嵌套,下面我们看看下面这段代码,
// 模拟 Ajax 请求:返回 Promise(真实项目里是 fetch/axios 等)
function requestData(url) {
return fetch(url)
.then(response => response.json()) // 解析 JSON 数据
.catch(error => console.error('请求失败:', error));
}
// 定义生成器函数 main:内部用 yield 暂停,写“同步风格”的异步逻辑
function* main() {
console.log('开始执行生成器,准备发请求');
// 1. yield 抛出 Promise(Ajax 请求),生成器暂停执行
// 这里的 result 会接收后续 next(data) 传进来的请求结果
const result = yield requestData('https://jsonplaceholder.typicode.com/todos/1');
// 3. 当外部调用 next(data) 后,生成器从暂停处恢复,执行这行
console.log('生成器内部拿到的请求结果:', result);
// 可以继续发第二个请求,依然是“同步写法”
const result2 = yield requestData(`https://jsonplaceholder.typicode.com/todos/${result.id + 1}`);
console.log('第二个请求结果:', result2);
}
// 外界执行生成器的逻辑
const generator = main(); // 1. 调用生成器,得到生成器对象(函数体不执行)
// 2. 第一次调用 next():生成器开始执行,直到第一个 yield 暂停
const firstNextResult = generator.next();
// firstNextResult.value 就是 yield 抛出的 Promise(requestData 的返回值)
const promise = firstNextResult.value;
// 3. 等 Promise 执行完成(请求返回),把结果传给生成器
promise.then(data => {
// 调用 next(data):把请求结果传进去,生成器从 yield 处恢复执行
generator.next(data);
// 第二个请求的处理(如果有):这里可以封装成自动执行逻辑,不用手动写
const secondNextResult = generator.next();
secondNextResult.value.then(data2 => {
generator.next(data2);
});
});
逐行解释(对应你的描述)
-
定义生成器函数
main:- 内部
yield requestData(...):requestData返回 Promise(Ajax 请求),yield会把这个 Promise 抛出去,同时暂停生成器的执行(不会立刻执行后面的console.log)。 -
const result = yield ...:这里的result暂时没有值,要等后续把请求结果传进来才会赋值。
- 内部
-
外界调用生成器:
-
const generator = main():调用生成器函数,只得到生成器对象,main的函数体完全不执行。 -
generator.next():第一次调用next,main开始执行,直到遇到yield暂停;next()返回的对象{ value: Promise, done: false }中,value就是yield抛出来的 Promise(Ajax 请求)。
-
-
处理 Promise 结果,恢复生成器:
- 给 Promise 加
then回调,等请求返回拿到data后,调用generator.next(data):-
data会作为上一个yield语句的返回值,赋值给main里的result; - 生成器从暂停的
yield位置继续执行,执行后面的console.log(result)—— 这一步就像“同步代码”一样,直接拿到了异步请求的结果。
-
- 给 Promise 加
为什么说“消灭了回调,近乎同步体验”
对比两种写法:
- 传统 Promise:请求结果的处理逻辑必须写在
then回调里(异步风格); - 生成器写法:
main内部没有任何then回调,const result = yield ...看起来就像“同步赋值”,拿到结果后直接console.log—— 代码结构和同步代码完全一致,只是多了yield关键字。
总结
- 核心目的:用生成器的
yield暂停特性,把异步 Promise 的“回调式”写法,改成生成器内部的“同步式”写法,消灭回调嵌套; - 核心流程:生成器
yield抛出 Promise → 等 Promise 执行完成 → 调用next(结果)把值传回生成器 → 生成器恢复执行,拿到结果;
等一下,怎么看着更复杂了??
不知道你发现没有发出疑问,经过转换,怎么比我写个嵌套更复杂了?别着急,我们一步步简化!
先看问题:手动处理多个 yield 的弊端
如果 main 里有 3 个 yield(3 次 Ajax 请求),手动处理会写成这样,重复代码特别多:
// 手动处理多个 yield 的糟糕写法(仅举例,不要这么写)
const gen = main();
// 处理第一个请求
let res1 = gen.next();
res1.value.then(data1 => {
// 处理第二个请求
let res2 = gen.next(data1);
res2.value.then(data2 => {
// 处理第三个请求
let res3 = gen.next(data2);
res3.value.then(data3 => {
gen.next(data3); // 最后一次 next
});
});
});
这种写法和回调嵌套没区别,完全违背了用生成器简化异步的初衷 —— 而递归自动执行器就是解决这个问题的关键。
核心思路:递归自动执行器
我们写一个通用的递归函数(比如叫 runGenerator),它会:
- 调用生成器的
next(),拿到返回结果(包含value(Promise)和done); - 判断
done:如果是true,递归终止; - 如果是
false,给value(Promise)加then回调; - 在
then里拿到异步结果,再递归调用自身,把结果传给next(),继续处理下一个yield。
完整代码示例(含递归执行器)
// 1. 模拟 Ajax 请求:返回 Promise
function requestData(url) {
return fetch(url)
.then(res => res.json())
.catch(err => console.error('请求失败:', err));
}
// 2. 定义有多个 yield 的生成器函数
function* main() {
console.log('开始第一个请求');
// 第一个 yield:请求 todo/1
const result1 = yield requestData('https://jsonplaceholder.typicode.com/todos/1');
console.log('第一个请求结果:', result1);
console.log('开始第二个请求');
// 第二个 yield:基于第一个结果请求 todo/2
const result2 = yield requestData(`https://jsonplaceholder.typicode.com/todos/${result1.id + 1}`);
console.log('第二个请求结果:', result2);
console.log('开始第三个请求');
// 第三个 yield:基于第二个结果请求 todo/3
const result3 = yield requestData(`https://jsonplaceholder.typicode.com/todos/${result2.id + 1}`);
console.log('第三个请求结果:', result3);
console.log('所有请求执行完毕');
return '最终结果';
}
// 3. 核心:递归自动执行器(通用函数,任何生成器都能用)
function runGenerator(generator) {
// 定义递归函数
function next(data) {
// 调用 next(),拿到当前执行结果({ value: Promise, done: boolean })
const result = generator.next(data);
// 终止条件:如果 done 为 true,递归结束
if (result.done) {
console.log('生成器执行完毕,最终返回值:', result.value);
return; // 终止递归
}
// 如果 done 为 false,处理当前的 Promise
result.value
.then(data => {
// 递归调用 next,把当前 Promise 的结果传给下一个 yield
next(data);
})
.catch(err => {
// 处理异常:也可以调用 generator.throw(err) 抛到生成器内部
console.error('异步执行出错:', err);
});
}
// 启动第一次递归(第一次 next 不传参,因为第一个 yield 左边没值可接)
next();
}
// 4. 启动执行器,自动处理所有 yield
runGenerator(main());
逐行解释递归执行器的逻辑
-
执行器初始化:调用
runGenerator(main()),先创建生成器对象,再执行next()(无参),启动第一次递归。 -
第一次递归:
-
generator.next()→ 执行main到第一个yield,返回{ value: 第一个请求的 Promise, done: false }; - 因为
done: false,给 Promise 加then; - 等第一个请求返回
data1,递归调用next(data1)。
-
-
第二次递归:
-
generator.next(data1)→data1赋值给result1,main执行到第二个yield,返回{ value: 第二个请求的 Promise, done: false }; - 给第二个 Promise 加
then,拿到data2后递归调用next(data2)。
-
-
第三次递归:
-
generator.next(data2)→data2赋值给result2,main执行到第三个yield,返回{ value: 第三个请求的 Promise, done: false }; - 拿到
data3后递归调用next(data3)。
-
-
第四次递归:
-
generator.next(data3)→data3赋值给result3,main执行到末尾,返回{ value: '最终结果', done: true }; - 检测到
done: true,递归终止。
-
为什么递归能解决问题?
-
自动迭代:不管
main里有多少个yield,递归都会自动处理,不用手动写每一次的next()和then(); -
终止条件清晰:靠
done: true判断生成器是否执行完,避免无限递归; -
代码复用:
runGenerator是通用函数,任何“yield 出 Promise”的生成器都能直接用,不用改逻辑。
生成器 + Promise 实现异步同步化 完整方案
再梳理这个“完整执行器”的核心逻辑
把我们之前的“简化版递归执行器”升级成工业级的 co 函数,完整逻辑如下(伪代码梳理):
// 1. 定义通用执行器 co
function co(generatorFunc) {
// 创建生成器对象
const generator = generatorFunc();
// 2. 定义递归处理函数 handlerResult
function handlerResult(result) {
// 终止条件:生成器执行完毕
if (result.done) {
return Promise.resolve(result.value); // 最终返回 Promise,对齐 async 函数
}
// 处理 Promise(成功/失败)
return Promise.resolve(result.value) // 确保 value 是 Promise(兼容非 Promise 情况)
.then(
// 成功回调:把结果传给 next,递归处理下一个 result
(data) => handlerResult(generator.next(data)),
// 失败回调:调用 throw() 抛异常给生成器内部
(error) => handlerResult(generator.throw(error))
);
}
// 3. 启动递归:传入第一次 next() 的结果
return handlerResult(generator.next());
}
// 4. 生成器内部用 try/catch 捕获异常
function* main() {
try {
const res1 = yield requestData('url1');
const res2 = yield requestData('url2');
} catch (err) {
console.log('捕获异常:', err); // 捕获 generator.throw() 抛的异常
}
}
// 5. 调用 co 执行生成器(通用复用)
co(main).then(finalRes => console.log('最终结果:', finalRes));
总结
这些内容,是我们之前讨论的生成器+Promise 实现异步同步化的“完整版落地”:
- 核心逻辑完全一致:递归执行器 + 暂停-恢复 + Promise 处理;
- 补充了关键细节:异常处理(
throw()+try/catch);
好的,该 async/await登场了
讲了这么多啊都没提到我们的主角,好的他们来了~
用 async/await 改写你这段手动代码
你手动处理 2 个请求的代码,换成 async/await 后长这样,没有任何手动的 next/then,但底层逻辑完全一致:
// 原生成器逻辑 → 等价的 async/await 代码
async function mainAsync() {
// 对应:第一次 next() + promise.then + generator.next(data)
const result = await requestData('https://jsonplaceholder.typicode.com/todos/1');
console.log('生成器内部拿到的请求结果:', result);
// 对应:secondNextResult = generator.next() + secondNextResult.value.then + generator.next(data2)
const result2 = await requestData(`https://jsonplaceholder.typicode.com/todos/${result.id + 1}`);
console.log('第二个请求结果:', result2);
}
// 调用 async 函数(引擎自动执行所有 next/then 逻辑)
mainAsync();
关键补充:async/await 不是“新东西”,是语法糖
async/await 并不是 JavaScript 新增的底层特性,它就是生成器 + 自动执行器的“语法糖”—— 本质上:
-
async函数 = 生成器函数 + 内置自动执行器; -
await关键字 =yield关键字的“语义化包装”(更易读,不用写yield); - 你手动写的递归执行器 → JS 引擎内置的、更高效的自动执行逻辑。
举个更直观的例子:引擎如何处理 await
当你写:
async function fn() {
const a = await Promise.resolve(1);
const b = await Promise.resolve(a + 1);
return b;
}
fn().then(res => console.log(res)); // 输出 2
JS 引擎在底层会做这些事(对应你的手动代码):
// 引擎模拟的底层逻辑(伪代码)
function* fnGenerator() {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(a + 1);
return b;
}
// 引擎内置的自动执行器(类似你写的递归函数)
function autoRun(generator) {
const gen = generator();
function next(data) {
const { value, done } = gen.next(data);
if (done) return Promise.resolve(value);
return value.then(res => next(res));
}
return next();
}
// 引擎自动调用
autoRun(fnGenerator).then(res => console.log(res)); // 输出 2
总结
- 你手动写的
next()、then()、传参、处理下一个yield这些操作,全部由async/await自动完成,不用你写一行; -
async/await是生成器+自动执行器的语法糖,核心逻辑和你手动封装的递归执行器完全一致; - 区别仅在于:引擎的内置执行器更高效、更健壮(处理了异常、中断等边界情况),而你手动写的是简化版。
简单说:async/await 就是把你手动做的“脏活累活”(调 next、等 Promise、传结果)全部自动化了,让你只需要写“看起来像同步”的代码就行。
async/await用了哪些设计模式呢?
async/await 并不是单一的设计模式,而是组合了多个经典模式的“语法糖封装”,核心是这3个:
| 设计模式 | 对应 async/await 的角色 |
|---|---|
| 迭代器模式(Iterator) | 生成器(Generator)本身就是迭代器的一种实现,next() 方法就是迭代器的核心(一步步“迭代”执行异步任务); |
| 观察者模式(Observer) | Promise 是典型的观察者模式(then/catch 监听 Promise 状态变化),await 本质是“监听 Promise 完成后通知生成器继续执行”; |
| 模板方法模式(Template Method) | JS 引擎内置的“自动执行器”就是模板方法:把“调用 next → 等 Promise → 传参 → 递归”的固定逻辑封装成模板,async/await 使用者只需要写业务逻辑(await 后面的异步任务),不用关心执行流程; |
关于设计模式,我应该会拉出一个主题好好研究
现在你有没有理解我说的“里应外合“
先明确“里”和“外”的角色
| 角色 | 对应代码/逻辑 | 核心动作 |
|---|---|---|
| 里(生成器内部) |
async 函数里的 await 异步任务
|
1. 抛出 Promise 给“外”;2. 暂停等待;3. 接收“外”传回来的值继续执行 |
| 外(JS引擎/执行器) | 引擎内置的自动执行器 | 1. 接住“里”抛出来的 Promise;2. 等待 Promise 执行完成;3. 把结果传回“里”,触发继续执行 |
用“里应外合”拆解完整执行流程(一步一步对应)
我们用 await requestData() 为例,还原这个互动过程:
第一步:“里”先出招 —— 抛出 Promise,原地待命
async function fn() {
// 「里」的动作:
// 1. 执行 await requestData(),先调用 requestData 得到 Promise;
// 2. 把这个 Promise “扔”给外面的引擎;
// 3. 自己暂停执行(就像士兵原地待命,等外面的消息);
const res = await requestData('url');
console.log(res); // 暂停后,这行暂时不执行
}
👉 对应“里应外合”:里先“应”—— 抛出异步任务(Promise),告诉外面“我要等这个做完”。
第二步:“外”接招 —— 处理 Promise,等结果
JS 引擎(外面的执行器)接住这个 Promise 后,不会闲着:
- 监听 Promise 的状态变化(成功/失败);
- 等待异步任务完成(比如接口请求返回数据);
- 拿到结果(比如接口返回的 data)。
👉 对应“里应外合”:外“合”—— 响应里面的请求,处理异步任务,拿到结果。
第三步:“外”回传结果 —— 给“里”赋值,触发继续执行
引擎拿到 data 后,会做两件关键事:
- 把 data 赋值给
await左边的变量res(相当于res = data); - 告诉“里”:“结果拿到了,你继续往下走!” —— 生成器从暂停的位置恢复执行。
async function fn() {
const res = await requestData('url');
// 「里」的动作:
// 接收到外面传的 data,赋值给 res,然后执行这行
console.log(res);
}
👉 对应“里应外合”:外把结果“合”进里面,里收到后继续执行,完成一次互动。
第四步:多轮“里应外合”(多个 await)
如果有多个 await,就是重复上面的过程:
async function fn() {
// 第一轮里应外合:
const res1 = await requestData('url1');
// 第二轮里应外合:
const res2 = await requestData(`url2?${res1.id}`);
}
👉 里抛第一个 Promise → 外处理 → 回传 res1 → 里再抛第二个 Promise → 外处理 → 回传 res2 → 直到执行完。
补充:异常场景的“里应外合”(更完整)
如果 Promise 失败(比如接口报错),互动逻辑变成:
- 里:抛出失败的 Promise;
- 外:接住失败的 Promise,拿到错误信息;
- 外:把错误“扔回”里(对应
generator.throw()); - 里:如果有
try/catch,就捕获这个错误,完成“合”。
async function fn() {
try {
const res = await requestData('error-url'); // 里抛出失败的 Promise
} catch (err) {
console.log(err); // 外把错误传回来,里捕获处理
}
}
记忆强化:用“里应外合”总结核心
- 里的核心:抛 Promise 等结果,接结果继续走(“应”—— 响应异步需求,等外部反馈);
- 外的核心:接 Promise 处理完,传结果促执行(“合”—— 配合内部需求,反馈处理结果);
-
async/await就是把这个“里应外合”的过程,从“手动写执行器”变成“引擎自动做”,你只需要写“里”的逻辑就行。
这个比喻完全抓住了本质 —— 生成器(里)和执行器(外)的互动,就是靠“抛 Promise-处理 Promise-传结果”完成的“里应外合”,记住这个比喻,就能记住 async/await 的底层执行逻辑了。
这下明白了 async/await 很多用法的底层逻辑
一、为什么必须有 async?—— 标记“异步执行器容器”
核心原因:
async 是给函数打一个“标记”,告诉 JS 引擎:这个函数内部有 await,需要按“生成器+自动执行器”的逻辑来处理,而不是普通函数的“一次性执行完”。
对应底层逻辑(里应外合):
- 没有
async的普通函数:调用后会“一口气执行完所有代码”,无法暂停; - 加了
async的函数:JS 引擎会把它当成「生成器+自动执行器」的封装体——调用async函数时,引擎先创建“生成器式”的执行上下文,为后续await的“暂停-恢复”铺路。
记忆例子:
// 错误:没有 async,用 await 会直接报错
function fn() {
const res = await requestData(); // Uncaught SyntaxError: await is only valid in async functions
}
// 正确:async 标记函数是“异步执行容器”
async function fn() {
const res = await requestData();
}
二、为什么要用 try/catch?—— 统一捕获“里应外合”的异常
核心原因:
await 后面的 Promise 一旦失败(rejected),底层会触发 generator.throw() 向函数内部抛异常——如果不捕获,这个异常会变成“未捕获异常”导致程序崩溃;而 try/catch 是同步代码的错误处理方式,async/await 把异步错误“伪装”成同步错误,自然用 try/catch 捕获最贴合直觉。
对应底层逻辑(里应外合):
- 外(引擎):发现 Promise 失败 → 调用
generator.throw(错误)把异常抛回“里”; - 里(async 函数):如果没有
try/catch,异常会从“里”逃出,变成全局未捕获异常;有try/catch则能在内部接住,和同步代码的错误处理完全一致。
记忆例子:
async function fn() {
try {
// await 后面的 Promise 失败 → 引擎抛异常到这里
const res = await Promise.reject(new Error('请求失败'));
} catch (err) {
console.log('捕获错误:', err.message); // 输出:请求失败
}
}
三、为什么说“await 后面的代码相当于 promise.then()”?—— 都是“等待完成后执行”
核心原因:
await 会暂停函数执行,等后面的 Promise 完成后,才执行 await 下面的代码——这和 promise.then(回调) 里“回调函数等 Promise 完成后执行”的逻辑完全一致,只是 await 把回调里的代码“扁平化”了。
对应底层逻辑(里应外合):
- Promise.then 写法:回调函数是“外”通知“里”执行的逻辑(观察者模式);
- await 写法:
await下面的代码,就是引擎自动帮你放到then回调里的逻辑,只是不用写回调嵌套。
记忆对比:
// Promise.then 写法(回调嵌套)
requestData().then(res => {
console.log(res); // 这行在 then 回调里,等 Promise 完成后执行
});
// async/await 写法(扁平化)
async function fn() {
const res = await requestData();
console.log(res); // 这行等价于上面 then 里的代码,等 Promise 完成后执行
}
四、为什么说“async 函数返回的是 Promise”?—— 对齐异步生态,兼容执行器逻辑
核心原因:
- 底层逻辑:
async函数的自动执行器最终要返回一个“结果容器”,而 Promise 是 JS 里唯一的“异步结果容器”——不管函数内部有没有await,引擎都会把返回值包装成 Promise; - 生态兼容:Promise 是 JS 异步的“通用语言”,返回 Promise 能让
async函数和现有异步逻辑(.then/.catch、Promise.all 等)无缝衔接。
记忆例子:
// 1. 显式返回普通值 → 自动包装成 Promise
async function fn1() {
return 123; // 等价于 return Promise.resolve(123)
}
fn1().then(res => console.log(res)); // 输出 123
// 2. 没有 return → 返回 Promise.resolve(undefined)
async function fn2() {
await requestData();
}
fn2().then(res => console.log(res)); // 输出 undefined
// 3. 抛出异常 → 返回 Promise.reject(错误)
async function fn3() {
throw new Error('出错了');
}
fn3().catch(err => console.log(err.message)); // 输出 出错了
五、补充:我经常混淆的点——“await 不是返回 Promise,是等待 Promise 并取其结果”
- ❌ 错误:await 返回 Promise;
- ✅ 正确:
await等待 后面的 Promise 完成,然后取出 Promise 的最终结果(成功值/失败抛异常)。
例子验证:
async function fn() {
// requestData() 返回 Promise,await 等待它完成,取出结果赋值给 res
const res = await requestData();
console.log(res); // res 是 Promise 的成功值(比如接口返回的 data),不是 Promise
}
对应到底层执行器的逻辑:
// 对应的生成器函数
function* genFn() {
const res = yield Promise.resolve(1); // await 的底层:yield 抛 Promise,等结果
console.log(res); // res = 1(next 传进来的)
return 3; // 生成器最终的 value = 3
}
// 执行器处理
autoRun(genFn).then(val => console.log(val)); // 输出 3
// autoRun 里的逻辑:
// - 第一次 next():yield 出 Promise.resolve(1),等待完成后 next(1)
// - 第二次 next(1):res = 1,执行到 return 3,done = true
// - 触发 if (done),返回 Promise.resolve(3) → 这就是 async 函数的返回值
记忆口诀:await 取 Promise 的“值”,async 包返回值成“Promise”——前者是“拆包”,后者是“打包”。
总结(核心记忆点)
把这些“为什么”浓缩成4句话,记下来就彻底懂了:
-
async是“容器标记”:告诉引擎这个函数要按“生成器+自动执行器”逻辑跑; -
try/catch是“异常兜底”:接住引擎从外部抛回的 Promise 失败异常; -
await下面的代码 =then回调:都是等 Promise 完成后执行,只是写法更平; -
async函数返回 Promise:对齐异步生态,让结果能被.then/.catch处理。
本质上,async/await 所有的“规则”(必须加 async、用 try/catch 等),都是为了让“生成器+自动执行器”的底层逻辑,能以“同步代码”的形式呈现——这也是它最巧妙的地方。