Async-Await ≈ Generators + Promises

在这篇文章中,我将阐述 ES2017 的异步函数(async functions)本质上是如何在两个较旧的 JavaScript 特性之间“博弈”的:即生成器(generators)和 Promise。原文称这两者都是在 ES2016 规范中更早加入该语言的。

开始阅读之前 ..
  • 本文并非关于 Promise、生成器或异步函数的入门教程。
  • 本文的唯一目标是阐述如何利用 Promise 和生成器来实现异步函数。
  • 文中不会对异步函数与其他方案孰优孰劣发表任何观点。
  • 文中使用的代码示例是为了便于解释而精心构思(甚至略显刻意)的,切勿用于任何严肃的生产环境。
但是,为什么呢……?
既然现在原生环境已经支持异步函数了,那我们还有必要去深究它们的工作原理吗?
嗯,除了满足好奇心这个显而易见的理由外,还有一个重要原因是为了兼容旧平台。如果你希望使用了这些新特性的代码能在旧版浏览器或旧版 Node.js 上运行,你就必须借助像 Babel 这样的工具,将这些新特性“转译”为旧特性。
因此,当你需要阅读或调试这些经过转译的代码时,如果深刻理解异步函数是如何被拆解为生成器和 Promise 的,将会派上大用场。举个例子,下面是一个简单的异步函数:
// 一个异步函数 (ES2017)
async function foo() {
  await bar();
}

这个函数会被 Babel 转换为以下 ES2016 代码(暂时不必纠结于理解它,我们稍后会详细讲解):

//Transformed by Babel to ES2016.
let foo = (() => {
  var _ref = _asyncToGenerator(function*() {
    yield bar();
  });

  return function foo() {
    return _ref.apply(this, arguments);
  };
})();

function _asyncToGenerator(fn) {
  return function() {
    var gen = fn.apply(this, arguments);
    return new Promise(function(resolve, reject) {
      function step(key, arg) {
        try {
          var info = gen[key](arg);
          var value = info.value;
        } catch (error) {
          reject(error);
          return;
        }

        if (info.done) {
          resolve(value);
        } else {
          return Promise.resolve(value).then(
            function(value) {
              step("next", value);
            },
            function(err) {
              step("throw", err);
            }
          );
        }
      }
      return step("next");
    });
  };
}
它们看起来差别很大!然而,如果你理解了异步函数(async functions)的实际工作原理,这种转换就显得相当直观了。
另一个有趣的事实是,浏览器也是以类似的方式实现异步函数的,也就是说,它们将异步代码转换为使用生成器(generators)和 Promise 的形式,这与 Babel 的转换方式非常相似。
 
好的,那么这是怎么发生的呢?
有时候,为了理解某样东西是如何运作的,最好的方法就是自己动手去构建它。所以让我们换个角度思考:
想象一下,我们得到了一段使用异步函数(async functions)的代码,我们要如何仅用 Promise 和生成器函数(generator functions)来重写它呢?
下面是async函数:
// An async function.
async function init() {

  // Perform asynchronous task1 and logs the result.
  const res1 = await doTask1();
  console.log(res1);

  // Perform asynchronous task2 using the result of task1.
  // Then log the result.
  const res2 = await doTask2(res1);
  console.log(res2);

  // Perform asynchronous task3 using the result of task2.
  // Then log the result.
  const res3 = await doTask3(res2);
  console.log(res3);

  // Finally return the result of task3.
  return res3;
}
它依次执行三个异步任务,其中每个任务都依赖于前一个任务的完成。最后,它返回最后一个任务的结果。
 
我们如何使用生成器(generators)来编写它?
生成器是一种可以退出并在稍后重新进入的函数。让我们快速回顾一下它们是如何工作的。以下是一个简单的生成器函数:
// A simple generator function.
function* gen() {
  const a = yield 1;
  const b = yield true;
  const c = yield "foo";
  console.log(a, b, c);
}
这个生成器函数 gen 有一些有趣的方面(摘自 MDN 文档):
  • 当调用一个生成器函数时,它的函数体并不会立即执行。相反,它会返回一个遵循迭代器协议(iterator protocol)的迭代器对象,即该对象拥有一个 next 方法。
  • 执行 gen 函数体的唯一方法是调用其迭代器对象上的 next 方法。每次调用 next 方法时,函数体会一直执行到遇到下一个 yield 表达式。该表达式的值会被从迭代器中返回。
  • 这个 next 方法还接受一个参数。带参数调用它时,会将当前的 yield 表达式替换为该参数,并恢复执行直到遇到下一个 yield 表达式。
 
为了阐明(非常、非常粗略地来说)..
  • 生成器函数是逐个 yield 执行的(即一次一个 yield 表达式),由其迭代器(next 方法)来驱动。
  • 每一个 yield 都有一种“给出 → 暂停 → 接收”的行为,可以这么说。
    • 它将当前 yield 表达式的值给出给迭代器。
    • 然后在这一点暂停,直到迭代器的 next 方法再次被调用。
    • next 方法再次被调用时,它从方法中接收参数,并将当前暂停的 yield 表达式替换为该参数。然后它移动到下一个 yield
你可能想要再次阅读上面的总结,或者参考极其棒的 MDN 文档!
 
但这对我们有什么帮助呢?
到现在你可能会想,生成器函数是如何解决我们的问题的呢?我们需要建模一个异步流程,必须等待某些任务完成后才能继续向前执行。但在我们目前的讨论中,一切都是同步的。我们要如何做到这一点呢?
  • 嗯,这里最重要的洞察是,生成器函数也可以 yield Promise 对象。
生成器函数可以产出一个 Promise(例如一个异步任务),并且可以通过控制其迭代器来暂停执行,直到这个 Promise 解决(resolve)或拒绝(reject),然后再使用解决后的值(或拒绝的值)继续执行。这种将迭代器与产出的 Promise 结合在一起的模式,使我们能够像这样来建模我们的需求:
// An implementation using a generator-function.
function* init() {

  // Yields a promise for task1.
  const res1 = yield doTask1();
  // Logs the result of the yield expression.
  console.log(res1);

  // Yields a promise for task2.
  const res2 = yield doTask2(res1);
  // Logs the result of the yield expression.
  console.log(res2);

  // Yields a promise for task3.
  const res3 = yield doTask3(res2);
  // Logs the result of the yield expression.
  console.log(res3);

  // Finally return it.
  return res3;
}
(注意这个生成器函数和我们的异步函数很像!) 
 
但这只是故事的一半。现在我们需要一种方法来执行它的函数体。我们需要一个函数,能够控制这个生成器函数的迭代器,使其在每次yield一个 Promise 时暂停,并在 Promise 解决(resolve)或拒绝(reject)后继续执行。这听起来可能很复杂,但其实非常容易实现,如下所示:
// This function takes a generator function
// & executes its body to completion.
function runner(genFn) {

  // Iterator for the generator function.
  const itr = genFn();

  // This function is called recursively once
  // the current promise is resolved.
  function run(arg) {
    const result = itr.next(arg);

    if (result.done) {
      // Return the current value if iteration is finished.
      return result.value;
    } else {
      // Execute the `run` function, only when the current promise resolves.
      return Promise.resolve(result.value).then(run);
    }
  }

  return run();
}
一个用于执行生成器函数的函数。(仅作解释说明,请勿使用!)
 
现在,我们可以使用这个runner函数来执行我们的生成器函数 init,如下所示:
 
// A generator-function for our requirement (same as before)
function* init() {
  const res1 = yield doTask1();
  console.log(res1);

  const res2 = yield doTask2(res1);
  console.log(res2);

  const res3 = yield doTask3(res2);
  console.log(res3);

  return res3;
}

// Use the `runner` function to execute `init`.
runner(init);
就这样!这种runner函数与generator函数的组合,实现了与原始异步函数(async function)类似的执行效果。
请注意,这个运行器函数仅用于演示概念。它并不适合任何严肃的生产环境使用。如果您正在寻找一个经过妥善实现的版本,您可以在这里找到它。
 
总结一下:
使用async-await进行完整实现,如下所示:
// 模拟异步任务1:返回一个Promise,模拟异步操作(如接口请求、文件读取)
function doTask1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('✅ 执行任务1完成');
            resolve('任务1结果:数据A'); // 异步成功后返回结果
        }, 1000); // 模拟1秒延迟
    });
}

// 模拟异步任务2:依赖任务1的结果
function doTask2(prevResult) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('✅ 执行任务2完成,依赖:', prevResult);
            resolve('任务2结果:数据B(基于' + prevResult + ')');
        }, 1000);
    });
}

// 模拟异步任务3:依赖任务2的结果
function doTask3(prevResult) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('✅ 执行任务3完成,依赖:', prevResult);
            resolve('任务3结果:数据C(基于' + prevResult + ')');
        }, 1000);
    });
}

// 主异步函数
async function init() {
    try {
        // 执行异步任务1并打印结果
        const res1 = await doTask1();
        console.log('📝 任务1输出:', res1);

        // 执行异步任务2(依赖任务1结果)并打印结果
        const res2 = await doTask2(res1);
        console.log('📝 任务2输出:', res2);

        // 执行异步任务3(依赖任务2结果)并打印结果
        const res3 = await doTask3(res2);
        console.log('📝 任务3输出:', res3);

        // 最终返回任务3结果
        console.log('\n🎉 所有任务执行完成!最终结果:', res3);
        return res3;

    } catch (error) {
        // 统一捕获异步过程中的错误
        console.error('❌ 执行失败:', error);
        throw error; // 抛出错误方便外部捕获
    }
}

// 调用执行主函数
init();

运行结果如下:

image

使用Generators + Promises重写async+await

// 模拟异步任务1:返回一个Promise,模拟异步操作(如接口请求、文件读取)
function doTask1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('✅ 执行任务1完成');
            resolve('任务1结果:数据A'); // 异步成功后返回结果
        }, 1000); // 模拟1秒延迟
    });
}

// 模拟异步任务2:依赖任务1的结果
function doTask2(prevResult) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('✅ 执行任务2完成,依赖:', prevResult);
            resolve('任务2结果:数据B(基于' + prevResult + ')');
        }, 1000);
    });
}

// 模拟异步任务3:依赖任务2的结果
function doTask3(prevResult) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log('✅ 执行任务3完成,依赖:', prevResult);
            resolve('任务3结果:数据C(基于' + prevResult + ')');
        }, 1000);
    });
}


// This function takes a generator function
// & executes its body to completion.
function runner(genFn) {

  // Iterator for the generator function.
  const itr = genFn();

  // 此处仅仅是run函数的定义
    // return run()是启动器,启动了run函数及其内部的迭代
  function run(arg) {
    const result = itr.next(arg);
        // 下面两行为测试代码
      console.log("arg is:",arg)
      console.log("result is:",result)

    if (result.done) {
      // Return the current value if iteration is finished.
      return result.value;
    } else {
        // Execute the `run` function, only when the current promise resolves.
        // 这一行是理解整个函数运行的关键点
      return Promise.resolve(result.value).then(run);
    }
  }

  // 没有 return run(),整个异步流程永远不会开始!
  return run();
}


// A generator-function for our requirement (same as before)
function* init() {
  const res1 = yield doTask1();
  console.log('📝 任务1输出:', res1);

  const res2 = yield doTask2(res1);
  console.log('📝 任务2输出:', res2);

  const res3 = yield doTask3(res2);
  console.log('📝 任务3输出:', res3);

  console.log('\n🎉 所有任务执行完成!最终结果:', res3);
  return res3;
}

// Use the `runner` function to execute `init`.
runner(init);

运行结果如下:

image

我们先是用 async 函数开了个头,随后又利用生成器(generators)和 Promise 写了一个一模一样的实现。也就是说,上面这两段代码的效果是类似的!

📚 进阶练习

  • 在本文开头,我们了解了 Babel 如何利用生成器(generators)和 Promise 将 async 代码转换为 ES2016 代码。现在你可以回头看看那段转换后的代码,对比一下我们的 runner 函数与 _asyncToGenerator 函数有多么相似。事实上,那个 _asyncToGenerator 函数就是我们这个极简版 runner 函数的“万无一失”的健壮版本。
  • 如果你依然兴致盎然,可以再往前迈一步,也就是将 async 函数转换为 ES2015 代码——也就是在不使用生成器的情况下实现。为此,你将不得不模拟生成器本身(例如使用带有 switch 分支的状态化死循环,可以参考 regenerator 项目)。
我希望这个解释能揭开 async 函数背后的神秘面纱。它们提供了更简洁的语法,从而减少了代码中的“噪音”。
 
posted @ 2026-03-28 14:05  chenlight  阅读(2)  评论(0)    收藏  举报