­

JavaScript中的异步生成器函数[每日前端夜话0xC9]

  • 2019 年 10 月 11 日
  • 筆記

作者:Valeri Karpov

翻译:疯狂的技术宅

来源:thecodebarbarian

TC39异步迭代器提案 将 for/await/of 引入了 JavaScript【http://thecodebarbarian.com/getting-started- with-async-iterators-in-node-js】,还介绍了异步生成器函数【https://github.com/tc39/proposal-async-iteration#async-generator-functions】的概念。现在 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 "HellonWorld"  (async () => {    for await (const val of asyncIterator) {      console.log(val); // Prints "Hello"    }  })();  

实际用例

你可能会想:“当 JavaScript 已经具有异步功能和生成器功能时,为什么还需要异步生成器功能?”一个用例是 Ryan Dahl 最初用 Node.js 来解决的经典进度条问题【https://stackoverflow.com/questions/31529013/nodejs-file-upload-with-progress-bar-using-core-nodejs-and-the-original-node-s】。

假设你要循环浏览 Mongoose cursor 【https://thecodebarbarian.com/cursors-in-mongoose-45】中的所有文档,并通过 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 【https://www.getrevue.co/profile/masteringjs/issues/framework-free-javascript-why-it-matters-188138】中报告其进度。无需显式创建 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 可观察变量默认情况下是冷操作【https://medium.com/codingthesmartway-com-blog/getting-started-with-rxjs-part-3-hot-and-cold-observables-4713757c9a88】,新的 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 报告异步函数的进度是一个很诱人的想法,因为它使你可以将业务逻辑与进度报告框架分离。下次需要实现进度条时,请试试异步生成器。 原文:http://thecodebarbarian.com/async-generator-functions-in-javascript