异步 Generator 函数

就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。

在语法上,异步 Generator 函数就是async函数与 Generator 函数的结合。

  1. async function* gen() {
  2. yield 'hello';
  3. }
  4. const genObj = gen();
  5. genObj.next().then(x => console.log(x));
  6. // { value: 'hello', done: false }

上面代码中,gen是一个异步 Generator 函数,执行后返回一个异步 Iterator 对象。对该对象调用next方法,返回一个 Promise 对象。

异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。

  1. // 同步 Generator 函数
  2. function* map(iterable, func) {
  3. const iter = iterable[Symbol.iterator]();
  4. while (true) {
  5. const {value, done} = iter.next();
  6. if (done) break;
  7. yield func(value);
  8. }
  9. }
  10. // 异步 Generator 函数
  11. async function* map(iterable, func) {
  12. const iter = iterable[Symbol.asyncIterator]();
  13. while (true) {
  14. const {value, done} = await iter.next();
  15. if (done) break;
  16. yield func(value);
  17. }
  18. }

上面代码中,map是一个 Generator 函数,第一个参数是可遍历对象iterable,第二个参数是一个回调函数funcmap的作用是将iterable每一步返回的值,使用func进行处理。上面有两个版本的map,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。

下面是另一个异步 Generator 函数的例子。

  1. async function* readLines(path) {
  2. let file = await fileOpen(path);
  3. try {
  4. while (!file.EOF) {
  5. yield await file.readLine();
  6. }
  7. } finally {
  8. await file.close();
  9. }
  10. }

上面代码中,异步操作前面使用await关键字标明,即await后面的操作,应该返回 Promise 对象。凡是使用yield关键字的地方,就是next方法停下来的地方,它后面的表达式的值(即await file.readLine()的值),会作为next()返回对象的value属性,这一点是与同步 Generator 函数一致的。

异步 Generator 函数内部,能够同时使用awaityield命令。可以这样理解,await命令用于将外部操作产生的值输入函数内部,yield命令用于将函数内部的值输出。

上面代码定义的异步 Generator 函数的用法如下。

  1. (async function () {
  2. for await (const line of readLines(filePath)) {
  3. console.log(line);
  4. }
  5. })()

异步 Generator 函数可以与for await...of循环结合起来使用。

  1. async function* prefixLines(asyncIterable) {
  2. for await (const line of asyncIterable) {
  3. yield '> ' + line;
  4. }
  5. }

异步 Generator 函数的返回值是一个异步 Iterator,即每次调用它的next方法,会返回一个 Promise 对象,也就是说,跟在yield命令后面的,应该是一个 Promise 对象。如果像上面那个例子那样,yield命令后面是一个字符串,会被自动包装成一个 Promise 对象。

  1. function fetchRandom() {
  2. const url = 'https://www.random.org/decimal-fractions/'
  3. + '?num=1&dec=10&col=1&format=plain&rnd=new';
  4. return fetch(url);
  5. }
  6. async function* asyncGenerator() {
  7. console.log('Start');
  8. const result = await fetchRandom(); // (A)
  9. yield 'Result: ' + await result.text(); // (B)
  10. console.log('Done');
  11. }
  12. const ag = asyncGenerator();
  13. ag.next().then(({value, done}) => {
  14. console.log(value);
  15. })

上面代码中,agasyncGenerator函数返回的异步遍历器对象。调用ag.next()以后,上面代码的执行顺序如下。

  1. ag.next()立刻返回一个 Promise 对象。
  2. asyncGenerator函数开始执行,打印出Start
  3. await命令返回一个 Promise 对象,asyncGenerator函数停在这里。
  4. A 处变成 fulfilled 状态,产生的值放入result变量,asyncGenerator函数继续往下执行。
  5. 函数在 B 处的yield暂停执行,一旦yield命令取到值,ag.next()返回的那个 Promise 对象变成 fulfilled 状态。
  6. ag.next()后面的then方法指定的回调函数开始执行。该回调函数的参数是一个对象{value, done},其中value的值是yield命令后面的那个表达式的值,done的值是false

A 和 B 两行的作用类似于下面的代码。

  1. return new Promise((resolve, reject) => {
  2. fetchRandom()
  3. .then(result => result.text())
  4. .then(result => {
  5. resolve({
  6. value: 'Result: ' + result,
  7. done: false,
  8. });
  9. });
  10. });

如果异步 Generator 函数抛出错误,会导致 Promise 对象的状态变为reject,然后抛出的错误被catch方法捕获。

  1. async function* asyncGenerator() {
  2. throw new Error('Problem!');
  3. }
  4. asyncGenerator()
  5. .next()
  6. .catch(err => console.log(err)); // Error: Problem!

注意,普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象。可以这样理解,async 函数和异步 Generator 函数,是封装异步操作的两种方法,都用来达到同一种目的。区别在于,前者自带执行器,后者通过for await...of执行,或者自己编写执行器。下面就是一个异步 Generator 函数的执行器。

  1. async function takeAsync(asyncIterable, count = Infinity) {
  2. const result = [];
  3. const iterator = asyncIterable[Symbol.asyncIterator]();
  4. while (result.length < count) {
  5. const {value, done} = await iterator.next();
  6. if (done) break;
  7. result.push(value);
  8. }
  9. return result;
  10. }

上面代码中,异步 Generator 函数产生的异步遍历器,会通过while循环自动执行,每当await iterator.next()完成,就会进入下一轮循环。一旦done属性变为true,就会跳出循环,异步遍历器执行结束。

下面是这个自动执行器的一个使用实例。

  1. async function f() {
  2. async function* gen() {
  3. yield 'a';
  4. yield 'b';
  5. yield 'c';
  6. }
  7. return await takeAsync(gen());
  8. }
  9. f().then(function (result) {
  10. console.log(result); // ['a', 'b', 'c']
  11. })

异步 Generator 函数出现以后,JavaScript 就有了四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不同之处。基本上,如果是一系列按照顺序执行的异步操作(比如读取文件,然后写入新内容,再存入硬盘),可以使用 async 函数;如果是一系列产生相同数据结构的异步操作(比如一行一行读取文件),可以使用异步 Generator 函数。

异步 Generator 函数也可以通过next方法的参数,接收外部传入的数据。

  1. const writer = openFile('someFile.txt');
  2. writer.next('hello'); // 立即执行
  3. writer.next('world'); // 立即执行
  4. await writer.return(); // 等待写入结束

上面代码中,openFile是一个异步 Generator 函数。next方法的参数,向该函数内部的操作传入数据。每次next方法都是同步执行的,最后的await命令用于等待整个写入操作结束。

最后,同步的数据结构,也可以使用异步 Generator 函数。

  1. async function* createAsyncIterable(syncIterable) {
  2. for (const elem of syncIterable) {
  3. yield elem;
  4. }
  5. }

上面代码中,由于没有异步操作,所以也就没有使用await关键字。