阅读视图

发现新文章,点击刷新页面。

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 }

关键特性解释

  1. 生成器函数定义function* 函数名() 是生成器函数的语法(* 的位置可以在 function 后、函数名前,空格不影响),这是和普通函数最核心的区别。
  2. 调用不执行:调用 numberGenerator() 不会执行函数体,只会返回一个 Generator 对象,这和普通函数调用立即执行完全不同。
  3. yield 暂停与返回
    • yield 是生成器的核心关键词,执行到 yield 时,函数会暂停执行(而非结束),并将 yield 后的值作为 next() 返回对象的 value
    • yield 不像 return 那样终止函数,下次调用 next() 会从暂停的位置继续执行。
  4. next 传参generator.next(参数) 传入的参数,会作为上一个 yield 语句的返回值(注意:第一个 next() 传参无效,因为此时还没有执行过任何 yield)。
  5. done 属性next() 返回对象的 done 属性表示生成器是否执行完毕:
    • false:生成器未执行完,还能继续调用 next() 获取值;
    • true:生成器执行完毕(执行到 return 或函数末尾),后续 next()valueundefined(这一点我经常忽略)。

总结

  1. 生成器函数通过 function* 定义,调用后返回生成器对象,而非立即执行函数体。
  2. yield 负责暂停函数并向外返回值,next() 负责恢复执行,且 next() 可传参作为上一个 yield 的返回值。
  3. 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() 方法核心逻辑解释

  1. throw() 的执行触发
    • next() 一样,调用 generator.throw() 会让生成器从上一次暂停的位置继续执行;
    • 但不同于 next() 传入普通参数,throw() 会在生成器当前执行位置主动抛出一个异常 -- 意思就是 从当前位置直接去走catch逻辑,try逻辑不会再走了
  2. 异常的捕获与处理
    • 如果生成器内部有 try/catch 块包裹了暂停位置后的执行逻辑,异常会被内部捕获,生成器不会立即终止;
    • 如果内部没有捕获,异常会抛出到外部,生成器状态变为 done: true,后续调用 next() 只会返回 { value: undefined, done: true }
  3. throw() 的返回值
    • 即使抛出了异常,只要内部捕获了,throw() 也会返回和 next() 一样的对象({ value, done });
    • 这个 value 是异常捕获后,生成器继续执行到下一个 yield 时返回的值(示例中就是 异常捕获后返回的兜底值)。

总结

  1. throw() 方法和 next() 一样能触发生成器继续执行,但核心作用是向生成器内部抛出异常
  2. 生成器内部可通过 try/catch 捕获 throw() 抛出的异常,捕获后生成器不会终止,仍可继续执行并返回新的 yield 值;
  3. 生成器的执行始终是“暂停-恢复”模式: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);
  });
});

逐行解释(对应你的描述)

  1. 定义生成器函数 main

    • 内部 yield requestData(...)requestData 返回 Promise(Ajax 请求),yield 会把这个 Promise 抛出去,同时暂停生成器的执行(不会立刻执行后面的 console.log)。
    • const result = yield ...:这里的 result 暂时没有值,要等后续把请求结果传进来才会赋值。
  2. 外界调用生成器

    • const generator = main():调用生成器函数,只得到生成器对象,main 的函数体完全不执行
    • generator.next():第一次调用 nextmain 开始执行,直到遇到 yield 暂停;next() 返回的对象 { value: Promise, done: false } 中,value 就是 yield 抛出来的 Promise(Ajax 请求)。
  3. 处理 Promise 结果,恢复生成器

    • 给 Promise 加 then 回调,等请求返回拿到 data 后,调用 generator.next(data)
      • data 会作为上一个 yield 语句的返回值,赋值给 main 里的 result
      • 生成器从暂停的 yield 位置继续执行,执行后面的 console.log(result) —— 这一步就像“同步代码”一样,直接拿到了异步请求的结果。

为什么说“消灭了回调,近乎同步体验”

对比两种写法:

  • 传统 Promise:请求结果的处理逻辑必须写在 then 回调里(异步风格);
  • 生成器写法:main 内部没有任何 then 回调,const result = yield ... 看起来就像“同步赋值”,拿到结果后直接 console.log —— 代码结构和同步代码完全一致,只是多了 yield 关键字。

总结

  1. 核心目的:用生成器的 yield 暂停特性,把异步 Promise 的“回调式”写法,改成生成器内部的“同步式”写法,消灭回调嵌套;
  2. 核心流程:生成器 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),它会:

  1. 调用生成器的 next(),拿到返回结果(包含 value(Promise)和 done);
  2. 判断 done:如果是 true,递归终止;
  3. 如果是 false,给 value(Promise)加 then 回调;
  4. 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());

逐行解释递归执行器的逻辑

  1. 执行器初始化:调用 runGenerator(main()),先创建生成器对象,再执行 next()(无参),启动第一次递归。
  2. 第一次递归
    • generator.next() → 执行 main 到第一个 yield,返回 { value: 第一个请求的 Promise, done: false }
    • 因为 done: false,给 Promise 加 then
    • 等第一个请求返回 data1,递归调用 next(data1)
  3. 第二次递归
    • generator.next(data1)data1 赋值给 result1main 执行到第二个 yield,返回 { value: 第二个请求的 Promise, done: false }
    • 给第二个 Promise 加 then,拿到 data2 后递归调用 next(data2)
  4. 第三次递归
    • generator.next(data2)data2 赋值给 result2main 执行到第三个 yield,返回 { value: 第三个请求的 Promise, done: false }
    • 拿到 data3 后递归调用 next(data3)
  5. 第四次递归
    • generator.next(data3)data3 赋值给 result3main 执行到末尾,返回 { 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 实现异步同步化的“完整版落地”:

  1. 核心逻辑完全一致:递归执行器 + 暂停-恢复 + Promise 处理;
  2. 补充了关键细节:异常处理(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 新增的底层特性,它就是生成器 + 自动执行器的“语法糖”—— 本质上:

  1. async 函数 = 生成器函数 + 内置自动执行器;
  2. await 关键字 = yield 关键字的“语义化包装”(更易读,不用写 yield);
  3. 你手动写的递归执行器 → 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

总结

  1. 你手动写的 next()then()、传参、处理下一个 yield 这些操作,全部由 async/await 自动完成,不用你写一行;
  2. async/await 是生成器+自动执行器的语法糖,核心逻辑和你手动封装的递归执行器完全一致;
  3. 区别仅在于:引擎的内置执行器更高效、更健壮(处理了异常、中断等边界情况),而你手动写的是简化版。

简单说: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 后,不会闲着:

  1. 监听 Promise 的状态变化(成功/失败);
  2. 等待异步任务完成(比如接口请求返回数据);
  3. 拿到结果(比如接口返回的 data)。

👉 对应“里应外合”:外“合”—— 响应里面的请求,处理异步任务,拿到结果

第三步:“外”回传结果 —— 给“里”赋值,触发继续执行

引擎拿到 data 后,会做两件关键事:

  1. 把 data 赋值给 await 左边的变量 res(相当于 res = data);
  2. 告诉“里”:“结果拿到了,你继续往下走!” —— 生成器从暂停的位置恢复执行。
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 失败(比如接口报错),互动逻辑变成:

  1. 里:抛出失败的 Promise;
  2. 外:接住失败的 Promise,拿到错误信息;
  3. 外:把错误“扔回”里(对应 generator.throw());
  4. 里:如果有 try/catch,就捕获这个错误,完成“合”。
async function fn() {
  try {
    const res = await requestData('error-url'); // 里抛出失败的 Promise
  } catch (err) {
    console.log(err); // 外把错误传回来,里捕获处理
  }
}

记忆强化:用“里应外合”总结核心

  1. 里的核心:抛 Promise 等结果,接结果继续走(“应”—— 响应异步需求,等外部反馈);
  2. 外的核心:接 Promise 处理完,传结果促执行(“合”—— 配合内部需求,反馈处理结果);
  3. 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”?—— 对齐异步生态,兼容执行器逻辑

核心原因:
  1. 底层逻辑:async 函数的自动执行器最终要返回一个“结果容器”,而 Promise 是 JS 里唯一的“异步结果容器”——不管函数内部有没有 await,引擎都会把返回值包装成 Promise;
  2. 生态兼容: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句话,记下来就彻底懂了:

  1. async 是“容器标记”:告诉引擎这个函数要按“生成器+自动执行器”逻辑跑;
  2. try/catch 是“异常兜底”:接住引擎从外部抛回的 Promise 失败异常;
  3. await 下面的代码 = then 回调:都是等 Promise 完成后执行,只是写法更平;
  4. async 函数返回 Promise:对齐异步生态,让结果能被 .then/.catch 处理。

本质上,async/await 所有的“规则”(必须加 async、用 try/catch 等),都是为了让“生成器+自动执行器”的底层逻辑,能以“同步代码”的形式呈现——这也是它最巧妙的地方。

❌