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:
OutputgeneratorFunction {<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 yield
s 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:
OutputNeoMorpheusTrinity
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 yield
s 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 yield
s 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:
Output1234
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:
Output0112358132134
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:
Output100200{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:
Output010203040
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.