Async Generator Functions in JavaScript

原文《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报告异步函数的进度是一个诱人的想法,因为它允许您将业务逻辑与进度报告框架分离。下次需要实现进度条时,给异步生成器一个机会。

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):