A proposta do TC39 de iteradores assíncronos que trouxe for/await/of
para o JavaScript também introduziu o conceito de uma função generator assíncrona . Agora, o JavaScript tem 6 tipos distintos de funções:
- Funções normais
function() {}
- Funções de seta
() => {}
- Funções assíncronas
async function() {}
- Funções de seta assíncrona
async () => {}
- Funções generator
function*() {}
- Funções generator assíncrona
async function*() {}
As funções generator assíncronas são especiais porque você pode usar ambos await
e yield
em uma função generator assíncrona. As funções generator assíncrona diferem das funções assíncrona e funções generator porque não retornam uma promessa ou um iterador, mas sim um iterador assíncrono. Você pode pensar em um iterador assíncrono como um iterador onde a função next()
sempre retorna uma promessa.
Sua primeira função generator assíncrona
As funções generator assíncronas se comportam de maneira semelhante às funções generator: a função generator retorna um objeto que tem uma função next()
e a chamada next()
executa a função generator até o próximo yield
. A diferença é que a função next()
de um iterador assíncrono retorna uma promessa .
Abaixo está um exemplo "Hello, World" com funções generator assíncrona. Observe que o script a seguir não funcionará em versões Node.js anteriores a 10.x.
'usze strict';
async function* run() {
// Dorme por 100ms, see: https://masteringjs.io/tutorials/fundamentals/sleep
await new Promise(resolve => setTimeout(resolve, 100));
yield 'Hello';
console.log('World');
}
// `run()` retorna um iterador assíncrono.
const asyncIterator = run();
// A função não é executada até `next()` ser chamado
asyncIterator.next().
then(obj => console.log(obj.value)). // Prints "Hello"
then(() => asyncIterator.next()); // Prints "World"
A maneira mais limpa de fazer um loop em todos os valores de uma função generator assíncrona é usando um for/await/of
.
'use strict';
async function* run() {
await new Promise(resolve => setTimeout(resolve, 100));
yield 'Hello';
console.log('World');
}
const asyncIterator = run();
// Imprimi "Hello\nWorld"
(async () => {
for await (const val of asyncIterator) {
console.log(val); // Imprimi "Hello"
}
})();
Um caso de uso prático
Você pode estar pensando "por que o JavaScript precisa de funções generator assíncronas quando já tem funções assíncronas e funções generator?" Um caso de uso é o problema clássico da barra de progresso em que Ryan Dahl originalmente escreveu Node.js para resolver .
Suponha que você deseja percorrer todos os documentos em um cursor Mongoose e relatar o progresso via websocket ou para a linha de comando.
'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}` });
}
// Supondo que você tenha vários documentos e você quer reportar o progresso
// de cada um. Você pode usar `yield` após processar cada documento.
const total = 5;
const cursor = Model.find().cursor();
let processed = 0;
for await (const doc of cursor) {
// Você pode pensar em `yield` como reportando: "Finalizei uma unidade de trabalho"
yield { processed: ++processed, total };
}
}
(async () => {
for await (const val of run()) {
// Imprimi "1 / 5", "2 / 5", "3 / 5", etc.
console.log(`${val.processed} / ${val.total}`);
}
})();
As funções generator assíncronas tornam mais fácil para sua função assíncrona relatar seu progresso de uma forma sem frameworks . Não há necessidade de criar explicitamente um websocket ou log no console - você pode lidar com isso separadamente se assumir que sua lógica de negócios usa yield
para relatar seu progresso.
Com Observáveis
Os iteradores assíncronos são ótimos, mas há outra primitiva de simultaneidade com a qual as funções generator assíncronas se alinham bem: os observáveis RxJS.
'use strict';
const { Observable } = require('rxjs');
const mongoose = require('mongoose');
async function* run() {
// Mesmo código de antes
}
// Cria um observável que emite cada valor que o iterador assíncrono retorna
const observable = Observable.create(async (observer) => {
for await (const val of run()) {
observer.next(val);
}
});
// Imprimi "1 / 5", "2 / 5", "3 / 5", etc.
observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));
Existem duas diferenças principais entre usar um observável RxJS e um iterador assíncrono. Primeiro, no exemplo acima, o código que se conecta ao console subscribe()
é reativo, e não imperativo . Em outras palavras, o manipulador subscribe()
não tem como afetar o código no corpo da função assíncrona, ele simplesmente reage aos eventos. Ao usar um for/await/of
loop, você pode, por exemplo, adicionar uma pausa de 1 segundo antes de retomar a função generator assíncrona.
(async () => {
for await (const val of run()) {
// Imprimi "1 / 5", "2 / 5", "3 / 5", etc.
console.log(`${val.processed} / ${val.total}`);
// Adiciona 1 segundo de delay para cada instrução `yield`
await new Promise(resolve => setTimeout(resolve, 1000));
}
})();
A segunda é que, como os observáveis RxJS são frios por padrão , uma nova chamada subscribe()
executa novamente a função.
Finalizando
As funções generator assíncronas podem parecer de nicho e confusas no início, mas fornecem o que pode se tornar a solução nativa do JavaScript para problemas de "barra de progresso". Usar yield
para relatar o progresso de uma função assíncrona é uma ideia atraente porque permite que você desacople sua lógica de negócios de sua estrutura de relatório de progresso. Dê uma chance aos generators assíncronos na próxima vez que precisar implementar uma barra de progresso.
Créditos
- Async Generator Functions in JavaScript, escrito originalmente por Valeri Karpov.
Top comments (0)