原文《Async Generator Functions in JavaScript》
翻译:范小饭
将for/await/of引入javascript的TC39异步迭代器提议还引入了异步生成器函数的概念。现在,javascript有6种不同的函数类型。
- 普通函数
function() {}
- 箭头函数
() => {}
-
异步函数
async function() {}
- 异步箭头函数
async () => {}
- 生成器函数
function*() {}
- 异步生成器函数
async function*() {}
异步生成器函数是特别的,因为你可以在函数中同时使用await 和 yield, 异步生成器函数不同于异步函数和生成器函数,因为它不会返回promise或者iterator,而是返回异步迭代器,你可以把异步迭代器作为一个next()函数总是返回一个promise的迭代器。
你的第一个异步迭代器
异步迭代器函数行为与迭代器函数相似,迭代器函数返回一个有next函数的对象,调用next()将执行generator函数,直到下一个yield。不同的是,异步迭代器的next() 返回一个promise
下面是一个异步生成器函数的"Hello, World"的例子。请注意,下面的脚本在10.x之前的node.js版本上不起作用。
'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还需要异步生成器函数?一个用例是RyanDahl最初编写node.js来解决的经典进度条问题。
假设您希望遍历Mongoose光标中的所有文档,并通过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}`);
}
})();
异步生成器函数使您的异步函数能够以无框架的方式报告其进度,不需要明确的创建websocket或者console.log,假设业务逻辑使用yield进行进度报告,你可以使用yield单独处理。
可以观测的
异步迭代器很棒,但还有另一个并发原语,即异步生成器函数与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 observable和异步迭代器有2个关键区别,
1、在上面的示例中,在subscribe()中记录到控制台的代码是反应式的,而不是命令式的。换句话说,subscribe()处理程序无法影响异步函数体中的代码,它只对事件作出反应,当用一个for/await/of 循环,例如,当使用for/await/of循环时,可以在恢复异步生成器函数之前添加一秒钟的暂停
(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 observable在默认情况下是冷的,所以一个新的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}`));
Moving On
异步生成器函数一开始可能看起来令人困惑,但它们提供了可能成为javascript的进度条问题的本机解决方案,使用yield报告异步函数的进度是一个诱人的想法,因为它允许您将业务逻辑与进度报告框架分离。下次需要实现进度条时,给异步生成器一个机会。
网友评论