Uma rápida introdução

Bem-vindos ao maravilhoso mundo das funções do gerador e geradores assíncronos em JavaScript.

Você está prestes a aprender sobre uma das mais exóticas (de acordo com a maioria dos desenvolvedores) funcionalidade JavaScript.

Vamos começar!

Funções do gerador em JavaScript

O que é uma função do gerador?

Uma função do gerador (ECMAScript 2015) em JavaScript é um tipo especial de função síncrona que é capaz de parar e retomar a sua execução à vontade.

Em contraste com as funções regulares JavaScript, que são de fogo e esquecimento, funções geradoras também têm a habilidade de:

  • comunicar com o chamador através de um canal bidirecional.
  • manter seu contexto de execução (escopo) sobre chamadas subsequentes.

Você pode pensar em funções geradoras como em fechamentos em esteróides, mas as semelhanças param aqui!

Sua primeira função geradora

Para criar uma função geradora nós colocamos uma estrela * após a palavra-chave function:

function* generate() {//}

Nota: funções geradoras também podem assumir a forma de um método de classe, ou de uma expressão de função. Em contraste, as funções geradoras de setas não são permitidas.

Após dentro da função podemos usar yield para pausar a execução:

function* generate() { yield 33; yield 99;}

yield pausa a execução e retorna um objeto chamado Generator ao chamador. Este objeto é tanto um iterável, quanto um iterador ao mesmo tempo.

Vamos desmistificar estes conceitos.

Iterables e iteradores

Um objeto iterável em JavaScript é um objeto implementando Symbol.iterator. Aqui está um exemplo mínimo de iterável:

const iterable = { : function() { /* TODO: iterator */ }};

Após termos este objecto iterável, atribuímos uma função a para devolver um objecto iterador.

Soa a muita teoria, mas na prática um iterável é um objecto sobre o qual podemos fazer um loop com for...of (ECMAScript 2015). Mais sobre isto num minuto.

Você já deve saber um par de iterables em JavaScript: arrays e strings por exemplo são iterables:

for (const char of "hello") { console.log(char);}

Outros iterables conhecidos são Map e Set. for...of Também é útil para iteração sobre valores de um objeto:

const person = { name: "Juliana", surname: "Crain", age: 32};for (const value of Object.values(person)) { console.log(value);}

Apenas lembrar que qualquer propriedade marcada como enumerable: false não aparecerá na iteração:

const person = { name: "Juliana", surname: "Crain", age: 32};Object.defineProperty(person, "city", { enumerable: false, value: "London"});for (const value of Object.values(person)) { console.log(value);}// Juliana// Crain// 32

Agora, o problema com a nossa iterabilidade personalizada é que ela não pode ir longe sozinha sem um iterador.

Iteradores também são objetos, mas eles devem estar de acordo com o protocolo de iteradores. Em resumo, os iteradores devem ter pelo menos um método next().

next() devem retornar outro objeto, cujas propriedades são value e done.

a lógica do nosso método next() deve obedecer as seguintes regras:

  • devolver um objeto com done: false para continuar a iteração.
  • devolver um objeto com done: true para parar a iteração.

value em vez disso, devemos manter o resultado que queremos produzir para o consumidor.

>

Vamos expandir nosso exemplo adicionando o iterador:

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};

Aqui temos um iterável, que implementa corretamente Symbol.iterator. Também temos um iterador, que retorna:

  • um objecto cuja forma é { value: x, done: false} até count atingir 3.
  • um objecto cuja forma é { value: x, done: true} quando count atingir 3.

Este mínimo iterável está pronto para ser iterado com for...of:

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};for (const iterableElement of iterable) { console.log(iterableElement);}

O resultado será:

123

Após você conhecer melhor as funções do gerador, você verá que os iteradores são a base para os objetos do gerador.

Confiar em mente:

  • iteradores são os objectos sobre os quais iteramos sobre.
  • iteradores são as coisas que tornam os iteradores … “iterável” sobre.

No final, para que serve um iterável?

Agora temos um laço padrão, confiável for...of que funciona virtualmente para quase qualquer estrutura de dados, personalizada ou nativa, em JavaScript.

Para usar for...of na sua estrutura de dados personalizada você tem que:

  • implementar Symbol.iterator.
  • prover um objeto iterador.

É isso!

Outros recursos:

  • Iteradores que vão iterar por Jake Archibald.

Iterables são espalháveis e destrutíveis

Além de for...of, também podemos usar espalhamento e desestruturação em iterables finitos.

Consulte novamente o exemplo anterior:

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};

Para retirar todos os valores que podemos espalhar o iterável em um array:

const values = ;console.log(values); // 

Para retirar apenas um par de valores em vez disso, podemos desestruturar o iterável. Aqui obtemos o primeiro e segundo valores da iterável:

const = iterable;console.log(first); // 1console.log(second); // 2

Aqui em vez disso obtemos o primeiro, e o terceiro:

const = iterable;console.log(first); // 1console.log(third); // 3

Vamos agora voltar o nosso foco nas funções do gerador.

Extrair dados de uma função do gerador

A partir do momento em que uma função do gerador está no lugar, podemos começar a interagir com ela. Esta interação consiste em:

  • getting values from the generator, step by step.
  • opcionalmente enviando valores de volta ao gerador.

Para extrair valores de um gerador podemos usar três abordagens:

  • chamando next() sobre o objeto iterator.
  • iteração com for...of.
  • espalhamento e desestruturação de array.

Uma função do gerador não calcula todos os seus resultados em um único passo, como fazem as funções regulares.

Se tomarmos o nosso exemplo, para obter valores do gerador podemos antes de tudo aquecer o gerador:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();

Aqui go torna-se o nosso objecto iterável/iterador, o resultado de chamar generate.

(Lembre-se, um objeto Generator é tanto um iterável quanto um iterador).

A partir de agora podemos chamar go.next() para avançar a execução:

function* generate() { yield 33; yield 99;}const go = generate();// Consume the generatorconst { value: firstStep } = go.next(); // firstStep is 33const { value: secondStep } = go.next(); // secondStep is 99

Aqui cada chamada para go.next() produz um novo objeto. No exemplo nós desestruturamos a propriedade value a partir deste objeto.

Objetos retornados da chamada next() no objeto iterador têm duas propriedades:

  • value: o valor para o passo atual.
  • done: um booleano indicando se há mais valores no gerador, ou não.

Implementamos tal objeto iterador na seção anterior. Ao usar um gerador, o objeto iterator já está lá para você.

next() funciona bem para extrair dados finitos de um objeto iterator.

Para iterar sobre dados não finitos, podemos usar for...of. Aqui está um gerador sem fim:

function* endlessGenerator() { let counter = 0; while (true) { counter++; yield counter; }}// Consume the generatorfor (const value of endlessGenerator()) { console.log(value);}

Como você pode notar, não há necessidade de inicializar o gerador ao usar for...of.

Finalmente, também podemos espalhar e desestruturar o objeto gerador. Aqui está o espalhamento:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); // 

Aqui a desestruturação:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Destructuringconst = go;console.log(first); // 1console.log(second); // 2

É importante notar que os geradores se esgotam uma vez que você consome todos os seus valores.

Se você espalhar os valores de um gerador, não há mais nada para extrair depois:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); // // Exhaustconst = go;console.log(first); // undefinedconsole.log(second); // undefined

Geradores também funcionam ao contrário: eles podem aceitar valores ou comandos do chamador, como veremos em um minuto.

>

Voltando às funções do gerador

Objetos do iterador, o objeto resultante de uma chamada de função do gerador, expõe os seguintes métodos:

  • next()
  • return()
  • throw()>

Já vimos next(), o que ajuda a puxar objetos de um gerador.

Não se limita apenas a extrair dados, mas também a enviar valores para o gerador.

Considerar o seguinte gerador:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}

Lista um parâmetro string. Fornecemos este argumento sobre inicialização:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");

Assim que chamamos next() pela primeira vez no objeto iterator, a execução inicia e produz “A”:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // A

Aqui podemos conversar com o gerador, fornecendo um argumento para next:

go.next("b");

Aqui está a lista completa:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // Aconsole.log(go.next("b").value); // B

A partir de agora, podemos alimentar valores para yield sempre que precisarmos de uma nova string em maiúsculas:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // Aconsole.log(go.next("b").value); // Bconsole.log(go.next("c").value); // Cconsole.log(go.next("d").value); // D

Se a qualquer momento quisermos voltar da execução, podemos usar return no objeto iterator:

const { value } = go.return("stop it");console.log(value); // stop it

Isto pára a execução.

Além dos valores você também pode lançar uma exceção no gerador. Veja “Tratamento de erros para funções do gerador”.

Casos de uso para funções do gerador

Mais desenvolvedores (eu incluído) vêem as funções do gerador como uma funcionalidade JavaScript exótica que tem pouca ou nenhuma aplicação no mundo real.

Isso pode ser verdade para o trabalho de front-end médio, onde um pouco de jQuery, e um pouco de CSS pode fazer truques na maioria das vezes.

Na realidade, as funções do gerador realmente brilham em todos aqueles cenários onde as performances são primordiais.

Em particular, elas são boas para:

  • trabalhar com grandes ficheiros e conjuntos de dados.
  • data wrangling on the back-end, ou no front-end.
  • gerar sequências infinitas de dados.
  • computar lógica cara on-demand.

As funções do gerador são também o bloco de construção de sofisticados padrões assíncronos com funções de gerador assíncrono, o nosso tópico para a próxima secção.

Funções do gerador assíncrono em JavaScript

O que é uma função do gerador assíncrono?

Uma função do gerador assíncrono (ECMAScript 2018) é um tipo especial de função assíncrona que é capaz de parar e retomar a sua execução à vontade.

A diferença entre funções do gerador síncrono e funções do gerador assíncrono é que estas últimas retornam um resultado assíncrono, baseado em Promise do objeto iterator.

Tal como as funções do gerador, as funções do gerador assíncrono são capazes de:

  • comunicar com o chamador.
  • manter seu contexto de execução (escopo) sobre as chamadas subseqüentes.

Sua primeira função de gerador assíncrono

Para criar uma função de gerador assíncrono, declaramos uma função de gerador com a estrela *, prefixada com async:

async function* asyncGenerator() { //}

Na função podemos usar yield para pausar a execução:

async function* asyncGenerator() { yield 33; yield 99;}

Aqui yield pausa a execução e devolve um objeto chamado Generator ao chamador.

Este objeto é tanto um iterável, como um iterador ao mesmo tempo.

Recapitulemos estes conceitos para ver como eles se encaixam na terra assíncrona.

Iteráveis e iteradores assíncronos

Um iterável assíncrono em JavaScript é um objeto implementando Symbol.asyncIterator.

Aqui está um exemplo mínimo:

const asyncIterable = { : function() { /* TODO: iterator */ }};

Após termos este objecto iterável, atribuímos uma função a para devolver um objecto iterador.

O objeto iterador deve obedecer ao protocolo do iterador com um método de next() (como o iterador síncrono).

Vamos expandir nosso exemplo adicionando o iterador:

const asyncIterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return Promise.resolve({ value: count, done: false }); } return Promise.resolve({ value: count, done: true }); } }; }};

Este iterador é similar ao que construímos nas seções anteriores, desta vez a única diferença é que envolvemos o objeto retornado com Promise.resolve.

Neste ponto podemos fazer algo nestes moldes:

const go = asyncIterable();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));// 1// 2

Or com for await...of:

async function consumer() { for await (const asyncIterableElement of asyncIterable) { console.log(asyncIterableElement); }}consumer();// 1// 2// 3

Iteradores assíncronos e iteradores são a base para as funções do gerador assíncrono.

Voltemos agora novamente o nosso foco neles.

Extrair dados de um gerador assíncrono

As funções do gerador assíncrono não calculam todos os seus resultados num único passo, como fazem as funções regulares.

Em vez disso, retiramos valores passo a passo.

Após examinarmos os iteradores assíncronos e os iterables, não deve ser surpresa ver que para retirarmos um gerador assíncrono Promises podemos usar duas abordagens:

  • chamando next() sobre o objeto iterator.
  • a iteração de assimetria com for await...of.

No nosso exemplo inicial podemos fazer:

async function* asyncGenerator() { yield 33; yield 99;}const go = asyncGenerator();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));

A saída deste código é:

3399

A outra abordagem usa a iteração de assimetria com for await...of. Para usar a iteração de assimetria envolvemos o consumidor com uma função async

Aqui está o exemplo completo:

async function* asyncGenerator() { yield 33; yield 99;}async function consumer() { for await (const value of asyncGenerator()) { console.log(value); }}consumer();

for await...of funciona bem para extrair fluxos de dados não finitos.

Vejamos agora como enviar os dados de volta para o gerador.

>

Dando de volta às funções do gerador assíncrono

Consulte o seguinte gerador assíncrono:

>

async function* asyncGenerator(string) { while (true) { string = yield string.toUpperCase(); }}>

>

Tal como a máquina sem fim em maiúsculas do exemplo do gerador, podemos fornecer um argumento para next():

>async function* asyncEndlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}async function consumer() { const go = await asyncEndlessUppercase("a"); const { value: firstStep } = await go.next(); console.log(firstStep); const { value: secondStep } = await go.next("b"); console.log(secondStep); const { value: thirdStep } = await go.next("c"); console.log(thirdStep);}consumer();

Aí cada passo envia um novo valor para o gerador.

A saída deste código é:

ABC

Even se não há nada inerentemente assíncrono em toUpperCase(), você pode sentir o propósito deste padrão.

Se a qualquer momento quisermos sair da execução, podemos chamar return() no objeto iterator:

 const { value } = await go.return("stop it"); console.log(value); // stop it

Além dos valores, você também pode lançar uma exceção no gerador. Veja “Tratamento de erros para geradores assíncronos”.

Use casos para iterables assíncronos e funções do gerador assíncrono

Se as funções do gerador são boas para trabalhar em sincronia com arquivos grandes e seqüências infinitas, as funções do gerador assíncrono permitem toda uma nova terra de possibilidades para JavaScript.

Em particular, a iteração assíncrona facilita o consumo de fluxos legíveis. O objeto Response em Fetch expõe body como um fluxo legível com getReader(). Podemos envolver tal fluxo com um gerador assíncrono, e depois iterar sobre ele com for await...of.

Async iterators and generators by Jake Archibald tem um monte de bons exemplos.

Outros exemplos de streams são streams de requisição com Fetch.

Na hora de escrever não há nenhuma API de navegador implementando Symbol.asyncIterator, mas a especificação Stream vai mudar isto.

Em Nó.js a recente Stream API joga bem com geradores assíncronos e iteração assíncrona.

No futuro, seremos capazes de consumir e trabalhar sem problemas com fluxos escrevíveis, legíveis e transformadores no lado do cliente.

Outros recursos:

  • The Stream API.

Wrapping up

Key terms and concepts we covered in this post:

ECMAScript 2015:

  • iterable.
  • iterador.

Estes são os blocos de construção das funções do gerador.

ECMAScript 2018:

  • iterador assíncrono.
  • iterador assíncrono.

Estes são os blocos de construção das funções do gerador assíncrono.

Um bom entendimento dos iteradores e iteradores pode levar você a um longo caminho. Não é que você trabalhe com funções de gerador e funções de gerador assíncrono todos os dias, mas eles são uma boa habilidade para ter no seu cinto de ferramentas.

E você? Você já as usou?

Obrigado por ler e ficar ligado neste blog!

Deixe uma resposta

O seu endereço de email não será publicado.