从 babel 编译看 async/await

907 阅读9分钟

该系列是本人准备面试的笔记,或许有描述不当的地方,请在评论区指出,感激不尽。

其他篇章:

  1. Promise.try 和 Promise.withResolvers,你了解多少呢?
  2. 挑战ChatGPT提供的全网最复杂“事件循环”面试题
  3. Vue.nextTick 从v3.5.13追溯到v0.7.0
  4. Vue 怎么监听 Set,WeakSet,Map,WeakMap 变化?
  5. Vue 是怎么从<HelloWorld />、<component is='HelloWorld'>找到HelloWorld.vue

前言

上一篇,《【面试准备】Promise.try 和 Promise.withResolvers,你了解多少呢?》介绍了 Promise 的前世今生,今天来看看 async/await 的来龙去脉。

a4f0fac245534d04ad46d9750ddd1789.jfif

为什么有了 Promise,还会诞生 async/await

地球人都知道,在 JavaScript 中,Promise 的引入是为了更优雅地解决回调地狱问题。它通过 .then.catch 链式调用,使异步操作的可读性大大提升。然而,Promise 的链式调用依旧存在以下问题:

  1. 语法复杂性:即使链式调用已经简化了回调嵌套,但在多个异步任务间处理逻辑仍显冗长,特别是有条件或循环时,代码很快会变得难以维护。
fetchData()
  .then((data) => processData(data))
  .then((processedData) => saveData(processedData))
  .catch((err) => handleError(err));
  1. 错误处理局限:在 Promise 链中,必须特别注意在哪一步处理错误,稍有遗漏可能导致异常未捕获。
  2. 无法直观表示顺序:链式结构虽然流畅,但不如同步代码那样清晰直观,尤其是在涉及多个异步任务时。

因此,async/await 应运而生。它在 Promise 基础上,进一步优化了语法,使异步代码的写法更加接近同步逻辑,增强了代码的可读性。

79df8075406d484faf691beeb79c468a.jpeg

async/await 特点

  1. 语法简洁,结构清晰async/await 提供了更直观的代码书写方式,让异步代码看起来像同步代码,从而提高了代码的可读性。
  2. 基于 Promise 工作async 函数始终返回一个 Promise 对象。await 用于等待一个返回 Promise 的表达式完成,并获取其解析结果。
  3. 异步函数自动包装:在 async 函数中,无需手动创建和管理 Promise,async 会自动将函数的返回值封装为 Promise。
  4. 错误处理更加方便:使用 try...catch 可以直接捕获异步操作中的错误,无需像传统 Promise 那样单独使用 catch 方法。
  5. 局限性
    • await 必须在 async 函数中使用,不能单独在顶层代码中使用(虽然模块化支持顶层 await)。
    • 如果不小心,可能导致性能问题。例如,多个不依赖的异步任务被按顺序处理,而非并发执行。

关于 Top-level await,可点击链接查看具体提案,或者浏览《(建议收藏) 深入了解 Top-level await》。

babel 编译

async 是 ECMAScript 2017 (ES8) 引入的关键字,那它隐藏在底层的逻辑是怎么样的呢?让我们跟着 babel 走进它的内部世界,见识最原始的 async/await

我们用 babel 的 Try it out 编译以下代码:

function resolveAfter2Seconds() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling'); // first
  const result = await resolveAfter2Seconds();
  console.log(result); // third
}

function syncCall() {
  asyncCall();
  console.log('sync'); // second
}

syncCall();
  1. 首先,可以看到上述逻辑被编译为以下代码:
function resolveAfter2Seconds() {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve('resolved');
    }, 2000);
  });
}
function asyncCall() {
  return _asyncCall.apply(this, arguments);
}
function _asyncCall() {
  _asyncCall = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
    var result;
    return _regeneratorRuntime().wrap(function _callee$(_context) {
      while (1) switch (_context.prev = _context.next) {
        case 0:
          console.log('calling'); // first
          _context.next = 3;
          return resolveAfter2Seconds();
        case 3:
          result = _context.sent;
          console.log(result); // third
        case 5:
        case "end":
          return _context.stop();
      }
    }, _callee);
  }));
  return _asyncCall.apply(this, arguments);
}
function syncCall() {
  asyncCall();
  console.log('sync'); // second
}
syncCall();

Okay,我们一步步来看调用情况。

  1. 首先毋庸置疑是 syncCall -> asyncCall -> _asyncCall。
  2. 接着看 _asyncCall 内部逻辑,调用 _asyncToGenerator,接收一个参数,该参数由 _regeneratorRuntime().mark 函数调用生成
  3. _regeneratorRuntime().mark 这个方法将一个普通函数标记为生成器函数,表示它包含了异步操作,并且返回一个包含状态机的对象。
  4. _regeneratorRuntime().mark 方法接收一个函数,该函数返回了 _regeneratorRuntime().wrap 的结果。
  5. _regeneratorRuntime().wrap 将生成器函数包裹起来,创建一个可以正确处理异步流程的状态机。通过 yield 来处理异步操作,并且在每次 await 时暂停执行,等待异步操作的结果。
  6. 正式进入执行代码的流程,首先打印 'calling',表示函数已被调用。
  7. _context.next = 3 之后,yield resolveAfter2Seconds() 会调用一个异步函数(假设 resolveAfter2Seconds 是一个返回 Promise 的函数)。这个 yield 操作会暂停执行,并等待 resolveAfter2Seconds 完成后继续执行。
  8. 在等待 resolveAfter2Seconds 完成的过程中,会继续打印 'sync'
  9. resolveAfter2Seconds 解析完成后,_context.sent 获取到异步操作的结果并存储到 result 变量中。
  10. 执行完 await 后,console.log(result) 会输出异步操作的结果,resolveAfter2Seconds 返回了 'resolved',所以最终控制台会输出 'resolved'
  11. return _context.stop() 会结束生成器的执行,确保函数的执行完毕。

其中 _asyncToGeneratorasyncGeneratorStep 会不断调用 next,直到 done 为 true 后,将结果 resolve,如果中间 catch 到错误或者 Generator 调用了 throw,则 reject

  • _asyncToGenerator:将一个生成器函数转换为返回 Promise 的异步函数。
    • 包装生成器函数 n,使其在异步函数上下文中运行。
    • 调用 asyncGeneratorStep 函数逐步执行生成器的 nextthrow 操作。
function _asyncToGenerator(n) {
  return function () {
    var t = this,
      e = arguments
    return new Promise(function (r, o) {
      var a = n.apply(t, e)
      function _next(n) {
        asyncGeneratorStep(a, r, o, _next, _throw, 'next', n)
      }
      function _throw(n) {
        asyncGeneratorStep(a, r, o, _next, _throw, 'throw', n)
      }
      _next(void 0)
    })
  }
}
  • asyncGeneratorStep: 处理生成器的步骤,基于其当前状态返回值或抛出错误。
    • 调用生成器的特定方法(nextthrow),并根据结果判断生成器是否完成。
    • 若未完成,则包装返回值为一个 Promise,确保异步操作链的正确性。
function asyncGeneratorStep(n, t, e, r, o, a, c) {
  try {
    var i = n[a](c),
      u = i.value
  } catch (n) {
    return void e(n)
  }
  i.done ? t(u) : Promise.resolve(u).then(r, o)
}

  • _regeneratorRuntime:提供了生成器和异步函数运行所需的核心支持,是 Generator 的 polyfill。

    • wrap: 该函数将一个生成器函数(或打算作为生成器的函数)包装成一个实际的生成器,使用 Context 对象管理函数执行的状态。这对于在生成器执行期间保持其状态至关重要。
    • Generator: 代表生成器函数的原型,使其能够以暂停和恢复的方式执行代码。
    • makeInvokeMethod: 该方法处理生成器内部逻辑的调用,控制生成器的流程,处理 nextthrowreturn 操作。
    • AsyncIterator: 这是一个处理异步迭代的类。它定义了如何处理异步函数中的 yield。如果从异步生成器返回的值是一个 Promise,代码会先解析它,再继续执行。
    • GeneratorFunctionGeneratorFunctionPrototype 定义了生成器函数的结构和原型链。
    • AsyncIterator 是一个用于处理异步迭代的类,它使得生成器可以返回 Promise 并等待它们完成。
    • Context: 上下文对象,对于管理生成器的执行状态至关重要。它跟踪当前的状态(如 nextthrowreturn)、try/catch 块和生成器函数的流程控制。它确保生成器能够从停止的地方恢复执行。
    • dispatchExceptionabrupt: 这些方法帮助处理错误并控制生成器的流程(例如,当抛出异常时,它会沿着生成器的执行路径传播)。

image.png

Tips: 建议将 babel 编译后的代码复制到 VSCode 或者在浏览器运行,在 syncCall() 断点,借助 Step Into 一步步调用。

Generator

说实话,上面 babel 编译 Generator 的 polyfill,看得我一脸懵逼,脑子嗡嗡的。

unnamed.jpg

看完两位大佬的文章《ES6 系列之 Babel 将 Generator 编译成了什么样子》和《通过babel学习Generator的底层实现》,稍微懂了一点。


在 Promise 出现之前,处理异步操作的传统方式是嵌套回调,而 Generator 提供了另一种解决方案,通过暂停函数执行使其具有更高的可控性。

Generator 的优势:

  1. 暂停和恢复执行:允许函数在中途暂停,并在后续重新启动。
  2. 可组合性:与 Promise 搭配可以实现简化的异步流控制。

使用 function* 定义,调用后不会立即执行,而是返回一个迭代器对象。

  • 通过 yield 暂停函数执行。
  • 使用 next() 恢复执行并传递参数。

async/await 与 Generator 的对比

async/await 是对 Generator + Promise 的语法糖。async 函数实际上是使用了 Promise,而 await 的行为类似于 yield,但更加简洁,且无需手动管理 next

特性async/awaitGenerator
引入版本ES8ES6
定义方式async functionfunction*
暂停机制awaityield
自动化驱动自动驱动需要手动调用 next
错误处理支持 try/catch需要手动处理错误
返回值始终返回一个 PromiseIterator(惰性求值)
目标用途异步流控制暂停函数执行
内部机制基于 PromiseGenerator基于迭代器
适用场景更直观,专注于简化异步逻辑,适合处理复杂异步场景更通用的暂停机制,适合惰性迭代
function* fetchData() {
  try {
    const data = yield fetch('https://api.example.com/data');
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}
const it = fetchData();
it.next().value
  .then((result) => it.next(result))
  .catch((error) => it.throw(error));
async function fetchData() {
  try {
    const data = await fetch('https://api.example.com/data');
    console.log(await data.json());
  } catch (error) {
    console.error(error);
  }
}

实现 async/await

从上面 babel 编译后的代码可以知道,将 _asyncToGeneratorasyncGeneratorStep 结合起来就是答案。

function asyncToGenerator(fn) {
  return function() {
    const gen = fn.apply(this, arguments)

    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let result
        try {
          result = gen[key](arg)
        } catch (error) {
          return reject(error)
        }

        const { value, done } = result

        if (done) {
          return resolve(value)
        } else {
          return Promise.resolve(value).then(
            function onResolve(val) {
              step("next", val)
            },
            function onReject(err) {
              step("throw", err)
            },
          )
        }
      }
      step("next")
    })
  }
}

await-to-js

谈到 async/await,不得不提的一个项目就是 await-to-js

在使用 async/await 时,常常需要编写如下代码来捕获和处理错误:

javascript
复制代码
try {
    const result = await someAsyncFunction();
    // 处理结果
} catch (error) {
    // 处理错误
}

当代码中有多个异步操作时,这样的写法可能会显得冗长。

而且使用 try/catch 块时会引入额外的缩进,导致代码层级增加。

await-to-js 通过提供一个函数 to(),可以更简洁地捕获异步操作的结果和潜在的错误,而不需要反复使用 try/catch 块。

import to from 'await-to-js';

async function exampleFunction() {
    const [err, data] = await to(someAsyncFunction());

    if (err) {
        // 处理错误
        console.error(err);
        return;
    }

    // 处理成功的结果
    console.log(data);
}

最后

朋友们,有没有好玩的 Generator 项目推荐,让我学习一下。我觉得 Generator 暂停函数执行的机制很酷,有很大的可玩性。

记得点赞收藏评论一键三连~

1-1720751939.jpeg