TC39异步迭代器提案for/await/of 引入了 JavaScript,还介绍了异步生成器函数的概念。现在 JavaScript 有 6 种不同的函数类型:

  • 默认函数 function() {}
  • 箭头函数 () => {}
  • 异步函数 async function() {}
  • 异步箭头函数 async () => {}
  • 生成器函数 function*() {}
  • 异步生成器函数 async function*() {}

异步生成器函数非常特殊,因为你可以在异步生成器函数中同时使用 awaityield。异步生成器函数与异步函数和生成器函数的不同之处在于,它们不返回 promise 或迭代器,而是返回一个异步迭代器。你可以将异步迭代器视为 iterator,其 next() 函数始终会返回 promise。

你的第一个异步生成器函数

异步生成器函数的行为类似于生成器函数:生成器函数返回一个具有 next() 函数的对象,调用 next() 将执行生成器函数直到下一个 yield。不同之处在于异步迭代器的 next() 函数返回了一个 promise

下面是带有异步生成器功能的 “Hello, World” 例子。请注意,以下脚本不适用于 Node.js 10.x 之前的版本。

'use strict';

async function* run() {
  await new Promise(resolve => setTimeout(resolve, 100));
  yield 'Hello';
  console.log('World');
}

// `run()` returns an async iterator.
const asyncIterator = run();

// The function doesn't start running until you call `next()`
asyncIterator.next().
  then(obj => console.log(obj.value)). // Prints "Hello"
  then(() => asyncIterator.next());  // Prints "World"

遍历整个异步生成器函数的最干净方法是使用 for/await/of 循环。

'use strict';

async function* run() {
  await new Promise(resolve => setTimeout(resolve, 100));
  yield 'Hello';
  console.log('World');
}

const asyncIterator = run();

// Prints "Hello\nWorld"
(async () => {
  for await (const val of asyncIterator) {
    console.log(val); // Prints "Hello"
  }
})();

实际用例

你可能会想:“当 JavaScript 已经具有异步功能和生成器功能时,为什么还需要异步生成器功能?”一个用例是 Ryan Dahl 最初用 Node.js 来解决的经典进度条问题

假设你要循环浏览 Mongoose cursor 中的所有文档,并通过 websocket 或命令行报告进度。

'use strict';

const mongoose = require('mongoose');

async function* run() {
  await mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true });
  await mongoose.connection.dropDatabase();

  const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
  for (let i = 0; i < 5; ++i) {
    await Model.create({ name: `doc ${i}` });
  }

  // Suppose you have a lot of documents and you want to report when you process
  // each one. You can `yield` after processing each individual doc.
  const total = 5;
  const cursor = Model.find().cursor();

  let processed = 0;
  for await (const doc of cursor) {
    // You can think of `yield` as reporting "I'm done with one unit of work"
    yield { processed: ++processed, total };
  }
}

(async () => {
  for await (const val of run()) {
    // Prints "1 / 5", "2 / 5", "3 / 5", etc.
    console.log(`${val.processed} / ${val.total}`);
  }
})();

异步生成器函数使你的异步函数可以轻松地在 framework-free 中报告其进度。无需显式创建 websocket 或登录控制台 - 如果你的业务逻辑使用 yield 进行进度报告,则可以单独处理。

Observables

异步迭代器很棒,但是还有另一个并发原语:RxJS observables,异步生成器函数可以很好地与之配合。

'use strict';

const { Observable } = require('rxjs');
const mongoose = require('mongoose');

async function* run() {
  // Same as before
}

// Create an observable that emits each value the async generator yields
// to subscribers.
const observable = Observable.create(async (observer) => {
  for await (const val of run()) {
    observer.next(val);
  }
});

// Prints "1 / 5", "2 / 5", "3 / 5", etc.
observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));

使用可观察的 RxJS 与异步迭代器有两个主要区别。首先,在上面的示例中,在 subscribe() 中记录到控制台的代码是响应式的,而不是命令式的。换句话说,subscribe() handler 无法影响异步函数主体中的代码,它仅对事件做出反应。例如,使用 for/await/of 循环时,你可以在恢复异步生成器函数之前添加 1 秒的暂停时间。

(async () => {
  for await (const val of run()) {
    // Prints "1 / 5", "2 / 5", "3 / 5", etc.
    console.log(`${val.processed} / ${val.total}`);
    // This adds a 1 second delay to every `yield` statement.
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
})();

第二个是,由于 RxJS 可观察变量默认情况下是冷操作,新的 subscribe() 调用将重新执行该函数。

// Prints "1 / 5", "2 / 5", "3 / 5", etc.
observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));
// Kicks off a separate instance of `run()`
observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));

总结

异步生成器函数乍一看似乎有些小众并令人困惑,但是它们提供了为 JavaScript 解决进度条问题的本地解决方案。使用 yield 报告异步函数的进度是一个很诱人的想法,因为它使你可以将业务逻辑与进度报告框架分离。下次需要实现进度条时,请试试异步生成器。