Async Generator Functions in JavaScript

Posted by gardan06 on Mon, 19 Aug 2019 10:17:48 +0200

Original< Async Generator Functions in JavaScript>
Fan Xiaofan

The TC39 asynchronous iterator proposal for introducing for/await/of into javascript also introduces the concept of asynchronous generator function. Now, javascript has six different function types.

  • Ordinary function() {}
  • Arrow function () => {}
  • Asynchronous function async function() {}
  • Asynchronous arrow function async () => {}
  • Generator function *() {}
  • Async generator function *() {}

The asynchronous generator function is special because you can use await and yield in the function at the same time. The asynchronous generator function is different from the asynchronous function and generator function because it does not return promise or iterator, but asynchronous iterator. You can always return asynchronous iterator as a next () function. A promise iterator.

Your first asynchronous iterator

The asynchronous iterator function behaves similarly to the iterator function, which returns an object with a next function and calls next() to execute the generator function until the next yield. The difference is that next() of the asynchronous iterator returns a promise.

The following is an example of an asynchronous generator function called "Hello, World". Note that the following script does not work in the node.js version prior to 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"

The cleanest way is to use for/await/of to loop the entire asynchronous generator function.

'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"
  }
})();
A practical use case

You might wonder why JavaScript needs asynchronous generator functions with asynchronous functions and generator functions. One use case was Ryan Dahl's original writing of node.js to solve the classic progress bar problem.

Suppose you want to traverse all documents in the Mongoose cursor and report progress through WebSocket or the command line.

'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}`);
  }
})();

Asynchronous generator functions enable your asynchronous functions to report their progress in a frameless manner, without explicitly creating WebSockets or console.log. Assuming that business logic uses yield for progress reporting, you can use yield alone.

Observable

Asynchronous iterators are great, but there's another concurrent primitive, that is, the asynchronous generator function can also match RxJS observables very well.

'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}`));

There are two key differences between RxJS observable and asynchronous iterator.
1. In the example above, the code recorded in subscribe () to the console is reactive, not imperative. In other words, the subscribe () handler can't affect the code in the asynchronous function body. It only reacts to events. When using a for/await/of loop, for example, when using a for/await/of loop, you can add a pause of one second before restoring the asynchronous generator function.

(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));
  }
})();

The second is that because RXJS observable is cold by default, a new subscribe () call will re-execute the function.

// 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

Asynchronous generator functions may seem confusing at first, but they provide native solutions to progress bar problems that may become javascript. It's an attractive idea to use yield to report progress of asynchronous functions, because it allows you to separate business logic from the progress reporting framework. Next time you need to implement the progress bar, give the asynchronous generator a chance.

Topics: Mongoose Javascript MongoDB