Generators: funções pausáveis com yield
1. Fundamentos dos Generators
Generators são funções especiais no JavaScript que podem ser pausadas e retomadas sob demanda. Diferente de funções comuns que executam até o final e retornam um único valor, generators podem produzir múltiplos valores ao longo do tempo, mantendo seu estado interno entre as pausas.
A sintaxe básica utiliza function* (com asterisco) e a palavra-chave yield para pausar a execução:
function* contador() {
yield 1;
yield 2;
yield 3;
}
const gen = contador();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Cada generator retorna um objeto Generator que implementa o protocolo Iterator, com o método next(). O objeto retornado possui as propriedades value (o valor produzido) e done (indicando se o generator terminou). Generators também implementam Symbol.iterator, permitindo seu uso em loops for...of.
function* sequencia() {
yield 'a';
yield 'b';
yield 'c';
}
for (const letra of sequencia()) {
console.log(letra); // 'a', 'b', 'c'
}
2. Controle de Fluxo com Yield
O poder real dos generators está na comunicação bidirecional. Quando você chama next(valor), o valor passado é recebido como retorno da expressão yield dentro do generator:
function* pergunta() {
const nome = yield 'Qual é seu nome?';
const idade = yield `Olá ${nome}, qual sua idade?`;
return `${nome} tem ${idade} anos`;
}
const gen = pergunta();
console.log(gen.next().value); // 'Qual é seu nome?'
console.log(gen.next('Maria').value); // 'Olá Maria, qual sua idade?'
console.log(gen.next(28).value); // 'Maria tem 28 anos'
Isso permite que o generator "converse" com o código externo, recebendo dados durante sua execução.
3. Delegando Generators com yield*
O operador yield* delega a iteração para outro generator ou qualquer objeto iterável:
function* numerosPares() {
yield 2;
yield 4;
yield 6;
}
function* numerosImpares() {
yield 1;
yield 3;
yield 5;
}
function* todosNumeros() {
yield* numerosImpares();
yield* numerosPares();
yield* [7, 8, 9]; // Também funciona com arrays
}
console.log([...todosNumeros()]); // [1, 3, 5, 2, 4, 6, 7, 8, 9]
Isso permite compor pipelines de dados complexos a partir de generators menores e reutilizáveis.
4. Generators Assíncronos (Async Generators)
Para fluxos assíncronos, existem os async generators (async function*), que combinam Promises com a pausabilidade dos generators:
async function* fetchPaginas(urls) {
for (const url of urls) {
const response = await fetch(url);
const dados = await response.json();
yield dados;
}
}
// Consumindo com for await...of
async function processarDados() {
const urls = ['/api/page1', '/api/page2', '/api/page3'];
for await (const pagina of fetchPaginas(urls)) {
console.log('Página recebida:', pagina);
}
}
Async generators são ideais para streams de dados com backpressure natural, pois o consumidor controla quando solicitar o próximo item.
5. Padrões Avançados com Generators
Máquinas de estado:
function* maquinaEstados() {
let estado = 'inicial';
while (true) {
if (estado === 'inicial') {
estado = yield 'Pronto para começar';
} else if (estado === 'processando') {
estado = yield 'Processando dados...';
} else if (estado === 'finalizado') {
yield 'Operação completa';
return;
}
}
}
Lazy evaluation - sequências infinitas:
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
// Gera sob demanda sem consumir memória infinita
Tratamento de erros:
function* generatorSeguro() {
try {
const dados = yield 'Forneça dados';
if (!dados) throw new Error('Dados inválidos');
yield `Processado: ${dados}`;
} catch (erro) {
yield `Erro capturado: ${erro.message}`;
}
}
6. Generators no Ecossistema Node.js
Leitura lazy de arquivos grandes:
const fs = require('fs');
const readline = require('readline');
function* lerArquivoGrande(caminho) {
const stream = fs.createReadStream(caminho);
const rl = readline.createInterface({ input: stream });
for await (const linha of rl) {
yield linha;
}
}
// Processa linha a linha sem carregar tudo em memória
for (const linha of lerArquivoGrande('dados.txt')) {
console.log('Processando:', linha);
}
Paginação de APIs:
async function* paginacaoAPI(urlBase, maxPaginas = 10) {
let pagina = 1;
while (pagina <= maxPaginas) {
const response = await fetch(`${urlBase}?page=${pagina}`);
const dados = await response.json();
yield dados;
if (!dados.hasNext) break;
pagina++;
}
}
// Uso com controle de fluxo explícito
const paginador = paginacaoAPI('https://api.exemplo.com/items');
for await (const pagina of paginador) {
console.log(`Processando página com ${pagina.items.length} itens`);
}
7. Generators em React e Frontend
Listas infinitas em componentes React:
function* geradorItens() {
let id = 1;
while (true) {
yield { id, nome: `Item ${id}`, timestamp: Date.now() };
id++;
}
}
function ListaInfinita() {
const [itens, setItens] = useState([]);
const generatorRef = useRef(geradorItens());
const carregarMais = () => {
const novosItens = Array.from({ length: 10 }, () =>
generatorRef.current.next().value
);
setItens(prev => [...prev, ...novosItens]);
};
return (
<div>
{itens.map(item => <div key={item.id}>{item.nome}</div>)}
<button onClick={carregarMais}>Carregar mais</button>
</div>
);
}
Controle de animações com generators em hooks:
function* animacaoGenerator() {
let progresso = 0;
while (progresso <= 100) {
yield progresso;
progresso += 5;
}
}
function useAnimacao() {
const animRef = useRef(animacaoGenerator());
const [progresso, setProgresso] = useState(0);
const avancar = useCallback(() => {
const resultado = animRef.current.next();
if (!resultado.done) {
setProgresso(resultado.value);
}
}, []);
return { progresso, avancar };
}
8. Comparações e Boas Práticas
Quando usar generators vs. Promises vs. async/await:
- Generators: Para sequências de dados sob demanda, lazy evaluation, máquinas de estado
- Promises: Para operações assíncronas únicas
- async/await: Para fluxos assíncronos lineares e previsíveis
Performance e memória: Generators são eficientes para grandes conjuntos de dados porque produzem valores sob demanda, sem armazenar tudo em memória. No entanto, cada chamada a next() tem overhead comparado a loops tradicionais.
Armadilhas comuns:
- Reinicialização: Cada chamada a um generator cria uma nova instância. O estado não é compartilhado entre chamadas.
- Escopo: Variáveis dentro do generator persistem entre pausas, mas cuidado com closures.
- Memory leaks: Generators que nunca terminam (loops infinitos sem break) podem causar vazamentos se não forem descartados adequadamente.
// Boa prática: sempre consumir completamente ou descartar
function* generatorControlado() {
try {
while (true) {
yield Math.random();
if (algumaCondicao) break;
}
} finally {
console.log('Generator finalizado');
}
}
Generators são uma ferramenta poderosa no ecossistema JavaScript, oferecendo controle fino sobre fluxos de dados síncronos e assíncronos. Quando combinados com React, Node.js e padrões como Redux Saga, tornam-se essenciais para aplicações complexas que exigem lazy evaluation, máquinas de estado ou processamento de streams.
Referências
- MDN Web Docs - Iterators and Generators — Documentação oficial completa sobre generators e iteradores em JavaScript
- Exploring JS - Generators — Capítulo detalhado do livro "Exploring ES6" sobre generators e seu funcionamento interno
- Node.js Documentation - Streams — Documentação oficial do Node.js sobre streams, que podem ser implementados com generators
- Redux Saga - API Reference — Documentação oficial do Redux Saga, biblioteca que utiliza generators para effects declarativos
- 2ality - Async Generators — Artigo técnico aprofundado sobre async generators e for await...of
- JavaScript.info - Generators — Tutorial interativo e prático sobre generators com exemplos detalhados