Compreendendo Geradores em JavaScript

O autor selecionou o Open Internet/Free Speech Fund para receber uma doação como parte do programa Write for DOnations.

Introduction

No ECMAScript 2015, os geradores foram introduzidos à linguagem JavaScript. Um gerador é um processo que pode ser pausado e retomado e pode produzir múltiplos valores. Um gerador em JavaScript consiste em uma função gerador, que retorna um objeto iterável Generator.

Geradores podem manter o estado, fornecendo uma maneira eficiente de fazer iteradores, e são capazes de lidar com infinitos fluxos de dados, que podem ser usados para implementar scroll infinito no frontend de uma aplicação web, para operar em dados de ondas sonoras, e muito mais. Adicionalmente, quando usados com Promises, os geradores podem imitar a funcionalidade async/await, o que nos permite lidar com código assíncrono de uma forma mais direta e legível. Embora async/await seja uma forma mais prevalente de lidar com casos comuns e simples de uso assíncrono, como buscar dados de uma API, os geradores têm características mais avançadas que fazem valer a pena aprender como usá-los.

Neste artigo, vamos cobrir como criar funções de gerador, como iterar sobre Generator objetos, a diferença entre yield e return dentro de um gerador, e outros aspectos do trabalho com geradores.

Funções Geradoras

Uma função geradora é uma função que retorna um objeto Generator, e é definida pela palavra-chave function seguida por um asterisco (*), como mostrado a seguir:

// Generator function declarationfunction* generatorFunction() {}

Ocasionalmente, você verá o asterisco ao lado do nome da função, ao contrário da palavra-chave da função, como function *generatorFunction(). Isto funciona da mesma forma, mas function* é uma sintaxe mais amplamente aceita.

As funções do gerador também podem ser definidas em uma expressão, como funções regulares:

// Generator function expressionconst generatorFunction = function*() {}

Geradores podem até ser os métodos de um objeto ou classe:

Os exemplos ao longo deste artigo usarão a sintaxe de declaração da função do gerador.

Nota: Ao contrário das funções regulares, os geradores não podem ser construídos com a palavra-chave new, nem podem ser usados em conjunto com as funções de seta.

Agora você saiba como declarar funções do gerador, vamos olhar para os objetos iteráveis Generator que eles retornam.

Objetos Geradores

Tradicionalmente, funções em JavaScript rodam até a conclusão, e chamar uma função retornará um valor quando ela chegar na palavra-chave return. Se a palavra-chave return for omitida, uma função retornará implicitamente undefined.

No seguinte código, por exemplo, declaramos uma função sum() que retorna um valor que é a soma de dois argumentos inteiros:

// A regular function that sums two valuesfunction sum(a, b) { return a + b}

Chamar a função retorna um valor que é a soma dos argumentos:

const value = sum(5, 6) // 11

Uma função geradora, no entanto, não retorna um valor imediatamente, e retorna um objeto iterável Generator. No exemplo seguinte, declaramos uma função e lhe damos um único valor de retorno, como uma função padrão:

// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}

Quando invocamos a função do gerador, ela retornará o objeto Generator, que podemos atribuir a uma variável:

// Assign the Generator object to generatorconst generator = generatorFunction()

Se esta fosse uma função normal, esperaríamos que generator nos desse a string retornada na função. No entanto, o que realmente obtemos é um objeto em um estado suspended. Chamando generator irá, portanto, dar saída similar ao seguinte:

O objeto Generator retornado pela função é um iterador. Um iterador é um objeto que tem um método next() disponível, que é usado para iterar através de uma seqüência de valores. O método next() devolve um objeto com propriedades value e done. value representa o valor retornado, e done indica se o iterador passou por todos os seus valores ou não.

Sabendo isto, vamos chamar next() no nosso generator e obter o valor e estado atual do iterador:

// Call the next method on the Generator objectgenerator.next()

Isto dará o seguinte output:

Output
{value: "Hello, Generator!", done: true}

O valor retornado de chamar next() é Hello, Generator!, e o estado de done é true, porque este valor veio de um return que fechou o iterador. Como o iterador está feito, o estado da função gerador irá mudar de suspended para closed. Chamando generator novamente dará o seguinte:

Output
generatorFunction {<closed>}

Como de agora, só demonstramos como uma função do gerador pode ser uma forma mais complexa de obter o valor return de uma função. Mas as funções do gerador também têm características únicas que as distinguem das funções normais. Na próxima seção, vamos aprender sobre o operador yield e ver como um gerador pode pausar e retomar a execução.

Operadores de rendimento

Geradores introduzem uma nova palavra-chave para JavaScript: yield. yield pode pausar uma função do gerador e retornar o valor que segue yield, fornecendo uma maneira leve de iterar através de valores.

Neste exemplo, vamos pausar a função do gerador três vezes com valores diferentes, e retornar um valor no final. Então vamos atribuir o nosso objeto Generator à variável generator.

Agora, quando chamarmos next() na função do gerador, ele vai pausar toda vez que encontrar yield. done será ajustado para false após cada yield, indicando que o gerador ainda não terminou. Uma vez encontrado um return, ou não há mais yields encontrados na função, done irá virar para true, e o gerador será terminado.

Utiliza o método next() quatro vezes seguidas:

// Call next four timesgenerator.next()generator.next()generator.next()generator.next()

Estes darão as seguintes quatro linhas de saída em ordem:

Output
{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}

Nota que um gerador não requer um return; se omitido, a última iteração retornará {value: undefined, done: true}, assim como qualquer chamada subsequente para next() depois de um gerador ter terminado.

Iterating Over a Generator

Usando o método next(), nós iteramos manualmente através do objeto Generator, recebendo todas as propriedades value e done do objeto completo. Entretanto, assim como Array, Map, e Set, um Generator segue o protocolo de iteração, e pode ser iterado através de for...of:

// Iterate over Generator objectfor (const value of generator) { console.log(value)}

Isto retornará o seguinte:

Output
NeoMorpheusTrinity

O operador de propagação também pode ser usado para atribuir os valores de um Generator a um array.

// Create an array from the values of a Generator objectconst values = console.log(values)

Isto dará o seguinte array:

Output
(3)

Both spread e for...of não irá fatorar o return nos valores (neste caso, teria sido 'The Oracle').

Nota: Enquanto estes dois métodos são eficazes para trabalhar com geradores finitos, se um gerador está lidando com um fluxo de dados infinito, não será possível usar spread ou for...of diretamente sem criar um loop infinito.

Closing a Generator

Como vimos, um gerador pode ter sua propriedade done definida para true e seu status definido para closed, iterando através de todos os seus valores. Existem duas formas adicionais de cancelar imediatamente um gerador: com o método return(), e com o método throw().

Com return(), o gerador pode ser terminado em qualquer ponto, tal como se uma declaração return tivesse estado no corpo da função. Você pode passar um argumento para return(), ou deixá-lo em branco para um valor indefinido.

Para demonstrar return(), vamos criar um gerador com alguns yield valores mas não return na definição da função:

function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity'}const generator = generatorFunction()

O primeiro next() nos dará 'Neo', com done definido para false. Se invocarmos um método return() no objeto Generator logo em seguida, teremos agora o valor passado e done definido como true. Qualquer chamada adicional para next() dará a resposta padrão completa do gerador com um valor indefinido.

Para demonstrar isto, execute os três métodos seguintes em generator:

generator.next()generator.return('There is no spoon!')generator.next()

Isto dará os três resultados seguintes:

Output
{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}

O método return() forçou o objeto Generator a completar e a ignorar qualquer outro yield palavras-chave. Isto é particularmente útil na programação assíncrona quando você precisa tornar funções canceláveis, como a interrupção de uma solicitação web quando um usuário deseja executar uma ação diferente, pois não é possível cancelar diretamente uma Promise.

Se o corpo de uma função do gerador tem uma maneira de pegar e lidar com erros, você pode usar o método throw() para jogar um erro no gerador. Isto inicia o gerador, atira o erro e termina o gerador.

Para demonstrar isto, vamos colocar um try...catch dentro do corpo da função do gerador e registar um erro se for encontrado:

Agora, vamos executar o método next(), seguido de throw():

generator.next()generator.throw(new Error('Agent Smith!'))

Isto dará a seguinte saída:

Output
{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}

Usando throw(), nós injetamos um erro no gerador, que foi pego pelo try...catch e logado no console.

Métodos e Estados dos Objetos Geradores

A tabela seguinte mostra uma lista de métodos que podem ser usados em Generator objetos:

Método Descrição
next() Retorna o próximo valor em um gerador
return() Retorna um valor em um gerador e termina o gerador
throw() >Lança um erro e termina o gerador

A tabela seguinte lista os estados possíveis de um objecto Generator:

Status Descrição
suspended Gerator parou a execução mas não terminou
closed Gerator terminou ao encontrar um erro, retornando, ou iterando através de todos os valores

yield Delegação

Além do normal yield operador, os geradores também podem usar a expressão yield* para delegar outros valores a outro gerador. Quando o yield* é encontrado dentro de um gerador, ele irá dentro do gerador delegado e começará a iterar através de todos os yields até que esse gerador seja fechado. Isto pode ser usado para separar diferentes funções do gerador para organizar semanticamente o seu código, tendo todas as suas yields na ordem certa.

Para demonstrar, podemos criar duas funções do gerador, uma das quais irá yield* operar na outra:

Próximo, vamos iterar através da função begin() do gerador:

// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}

Esta dará os seguintes valores na ordem em que são gerados:

Output
1234

O gerador externo rendeu os valores 1 e 2, depois delegados ao outro gerador com yield*, que retornaram 3 e 4.

yield* também pode delegar a qualquer objecto que seja iterável, tal como um Array ou um Mapa. A delegação de rendimento pode ser útil na organização do código, uma vez que qualquer função dentro de um gerador que quisesse usar yield também teria de ser um gerador.

Fluxos de dados infinitos

Um dos aspectos úteis dos geradores é a capacidade de trabalhar com infinitos fluxos de dados e colecções. Isto pode ser demonstrado pela criação de um loop infinito dentro de uma função geradora que incrementa um número por um.

No seguinte bloco de código, definimos esta função do gerador e então iniciamos o gerador:

Agora, itere através dos valores usando next():

// Iterate through the valuescounter.next()counter.next()counter.next()counter.next()

Esta dará a seguinte saída:

Output
{value: 0, done: false}{value: 1, done: false}{value: 2, done: false}{value: 3, done: false}

A função retorna valores sucessivos no loop infinito enquanto a propriedade done permanece false, assegurando que não irá terminar.

Com geradores, você não precisa se preocupar em criar um loop infinito, pois você pode parar e retomar a execução à vontade. No entanto, você ainda tem que ter cuidado com a forma como você invoca o gerador. Se você usar spread ou for...of em um fluxo de dados infinito, você ainda estará iterando sobre um loop infinito de uma só vez, o que causará o travamento do ambiente.

Para um exemplo mais complexo de um fluxo de dados infinito, podemos criar uma função de gerador Fibonacci. A sequência Fibonacci, que adiciona continuamente os dois valores anteriores juntos, pode ser escrita usando um loop infinito dentro de um gerador da seguinte forma:

Para testar isso, podemos fazer um loop através de um número finito e imprimir a sequência Fibonacci no console.

// Print the first 10 values of fibonacciconst fib = fibonacci()for (let i = 0; i < 10; i++) { console.log(fib.next().value)}

Isso dará o seguinte:

Output
0112358132134

A capacidade de trabalhar com conjuntos de dados infinitos é uma parte do que torna os geradores tão poderosos. Isto pode ser útil para exemplos como implementar scroll infinito no frontend de uma aplicação web.

Passing Values in Generators

Atrás deste artigo, temos usado geradores como iteradores, e temos produzido valores em cada iteração. Além de produzir valores, os geradores também podem consumir valores de next(). Neste caso, yield conterá um valor.

É importante notar que o primeiro next() que é chamado não passará um valor, mas apenas iniciará o gerador. Para demonstrar isto, podemos registrar o valor de yield e chamar next() algumas vezes com alguns valores.

Isto dará a seguinte saída:

Output
100200{value: "The end", done: true}

Também é possível semear o gerador com um valor inicial. No exemplo seguinte, vamos fazer um laço de for e passar cada valor para o método next(), mas passar um argumento para a função inicial também:

Vamos recuperar o valor de next() e render um novo valor para a próxima iteração, que é o valor anterior vezes dez. Isto dará o seguinte:

Output
010203040

Uma outra forma de lidar com o arranque de um gerador é envolver o gerador numa função que chamará sempre next() uma vez antes de fazer qualquer outra coisa.

async/await with Generators

Uma função assíncrona é um tipo de função disponível em ES6+ JavaScript que torna o trabalho com dados assíncronos mais fácil de entender fazendo com que pareçam síncronos. Os geradores têm uma gama mais extensa de capacidades do que as funções assíncronas, mas são capazes de replicar comportamentos semelhantes. Implementar programação assíncrona desta forma pode aumentar a flexibilidade do seu código.

Nesta seção, vamos demonstrar um exemplo de reprodução async/await com geradores.

Let’s build an asynchronous function that uses the Fetch API to get data from the JSONPlaceholder API (que fornece dados JSON de exemplo para fins de teste) e registra a resposta para o console.

Inicie definindo uma função assíncrona chamada getUsers que busca dados da API e retorna uma array de objetos, então chame getUsers:

Isso dará dados JSON similares aos seguintes:

Utilizando geradores, podemos criar algo quase idêntico que não use as async/await palavras-chave. Em vez disso, ele usará uma nova função que criamos e yield valores em vez de await promessas.

No seguinte bloco de código, definimos uma função chamada getUsers que usa nossa nova função asyncAlt (que escreveremos mais tarde) para imitar async/await.

Como podemos ver, parece quase idêntico à implementação async/await, excepto que existe uma função geradora a ser passada em que se produzem valores.

Agora podemos criar uma função asyncAlt que se assemelha a uma função assíncrona. asyncAlt tem uma função geradora como parâmetro, que é a nossa função que produz as promessas que fetch devolve. asyncAlt retorna uma função em si, e resolve todas as promessas que encontra até a última:

Esta dará a mesma saída que a async/await versão:

Note que esta implementação é para demonstrar como os geradores podem ser usados no lugar de async/await, e não é um projeto pronto para produção. Não tem o tratamento de erros configurado, nem tem a capacidade de passar parâmetros para os valores produzidos. Embora este método possa adicionar flexibilidade ao seu código, muitas vezes async/await será uma melhor escolha, já que ele abstrai detalhes de implementação e permite que você se concentre em escrever código produtivo.

Conclusion

Generators são processos que podem parar e retomar a execução. Eles são uma característica poderosa e versátil do JavaScript, embora eles não sejam comumente usados. Neste tutorial, aprendemos sobre funções e objetos geradores, métodos disponíveis para geradores, os operadores yield e yield*, e geradores usados com conjuntos de dados finitos e infinitos. Também exploramos uma maneira de implementar código assíncrono sem callbacks aninhados ou longas cadeias de promessa.

Se você gostaria de aprender mais sobre a sintaxe JavaScript, dê uma olhada em nosso Understanding This, Bind, Call, and Apply in JavaScript and Understanding Map and Set Objects in JavaScript tutorials.

Deixe uma resposta

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