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:

  1. Reinicialização: Cada chamada a um generator cria uma nova instância. O estado não é compartilhado entre chamadas.
  2. Escopo: Variáveis dentro do generator persistem entre pausas, mas cuidado com closures.
  3. 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