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*() {}
异步生成器函数非常特殊,因为你可以在异步生成器函数中同时使用 await
和 yield
。异步生成器函数与异步函数和生成器函数的不同之处在于,它们不返回 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
