A iteração assíncrona é um conceito fundamental em JavaScript moderno que permite processar dados que chegam de forma assíncrona, como respostas de APIs, leitura de arquivos ou streams de dados. Diferente da iteração síncrona tradicional, aqui cada elemento pode não estar disponível imediatamente, exigindo que o código aguarde a chegada de cada valor. Nesta aula, você aprenderá a usar o for await...of para percorrer iteráveis assíncronos, entenderá o que são streams e como eles se relacionam com esse padrão, e verá casos de uso práticos que demonstram a importância dessa técnica.

Dominar a iteração assíncrona é essencial para escrever código eficiente e legível ao lidar com dados em tempo real, processamento de arquivos grandes ou qualquer operação que dependa de fontes assíncronas. Vamos mergulhar nos detalhes, começando pelo for await...of e depois explorando streams e aplicações.

for await...of

O for await...of é uma construção que permite iterar sobre objetos que são async iterables, ou seja, objetos que implementam o método [Symbol.asyncIterator]. Ele é semelhante ao for...of, mas em vez de obter valores síncronos, ele aguarda a resolução de cada promessa antes de executar o corpo do loop. Isso é particularmente útil quando você tem uma sequência de dados que chegam ao longo do tempo, como eventos, chunks de um arquivo ou respostas paginadas de uma API.

Para entender melhor, é importante conhecer a diferença entre iteráveis síncronos e assíncronos. Um iterável síncrono tem um método [Symbol.iterator] que retorna um iterador com um método next() que retorna { value, done } imediatamente. Já um iterável assíncrono tem [Symbol.asyncIterator] e o método next() retorna uma Promise que resolve para { value, done }. O for await...of automaticamente aguarda essa promessa, simplificando o consumo de dados assíncronos.

Vejamos um exemplo básico. Suponha que você tenha uma função que gera números com um atraso, simulando uma fonte assíncrona:

async function* geradorAssincrono() {
  for (let i = 0; i < 5; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

async function main() {
  for await (const numero of geradorAssincrono()) {
    console.log(numero);
  }
}

main();
// Saída: 0, 1, 2, 3, 4 (com intervalos de 100ms)

No exemplo, geradorAssincrono é uma função geradora assíncrona (marcada com async function*), que retorna um async iterable. O for await...of percorre cada valor produzido, aguardando a promessa de cada yield.

Uma característica importante é que o for await...of pode ser usado com qualquer async iterable, incluindo objetos que você mesmo criar. Por exemplo, você pode criar um objeto com um método [Symbol.asyncIterator] personalizado:

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let i = 0;
    return {
      next() {
        if (i < 3) {
          return new Promise(resolve => {
            setTimeout(() => resolve({ value: i++, done: false }), 100);
          });
        }
        return Promise.resolve({ done: true });
      }
    };
  }
};

async function consumir() {
  for await (const valor of asyncIterable) {
    console.log(valor);
  }
}

consumir();
// Saída: 0, 1, 2

Esse padrão é poderoso, mas na prática é mais comum usar geradores assíncronos ou APIs que já retornam async iterables, como a leitura de streams no Node.js.

Streams (introdução)

Streams são fluxos de dados que podem ser lidos ou escritos de forma contínua, em vez de carregar tudo na memória de uma vez. Em JavaScript, existem duas principais implementações de streams: uma no Node.js (streams nativas) e uma na Web (Streams API). Ambas são projetadas para lidar com dados assíncronos e são consumidas frequentemente com for await...of.

No Node.js, por exemplo, a leitura de um arquivo grande pode ser feita usando createReadStream, que retorna um stream legível. Esse stream é um async iterable, permitindo que você processe cada chunk à medida que chega, sem bloquear a memória. Veja um exemplo:

const fs = require('fs');

async function lerArquivo(caminho) {
  const stream = fs.createReadStream(caminho, { encoding: 'utf8' });
  for await (const chunk of stream) {
    console.log('Chunk recebido:', chunk.length, 'caracteres');
  }
}

lerArquivo('arquivo.txt');

Na Web, a Streams API define ReadableStream, WritableStream e TransformStream. Um ReadableStream também é um async iterable, e você pode consumi-lo com for await...of. Por exemplo, ao buscar um recurso na rede com fetch, o corpo da resposta pode ser lido como um stream:

const resposta = await fetch('https://exemplo.com/dados');
const reader = resposta.body.getReader();

// Consumindo o stream com for await...of
for await (const chunk of reader) {
  console.log(new TextDecoder().decode(chunk));
}

No entanto, o reader retornado por getReader() não é diretamente um async iterable; você precisa usar o método read() manualmente ou usar o helper streamAsyncIterator (disponível em alguns ambientes) ou o for await...of diretamente se o stream for configurado como async iterable. Na prática, muitos ambientes já permitem for await...of em ReadableStream diretamente, mas é importante verificar a compatibilidade.

Uma das vantagens dos streams é o processamento incremental, que reduz o uso de memória e permite iniciar o processamento antes de todos os dados chegarem. Isso é crucial para grandes arquivos, vídeos, logs em tempo real, etc.

Casos de uso

A iteração assíncrona é amplamente utilizada em diversos cenários. Um dos mais comuns é o processamento de arquivos grandes, onde você lê o arquivo em chunks e processa cada um sem carregar o arquivo inteiro na memória. Isso é essencial em aplicações Node.js que lidam com logs, uploads de arquivos ou bancos de dados que exportam dados em lotes.

Outro caso de uso é o consumo de APIs paginadas. Muitas APIs retornam dados em páginas, e você precisa fazer várias requisições para obter todos os dados. Com um gerador assíncrono, você pode encapsular a lógica de paginação e iterar sobre todas as páginas de forma transparente:

async function* paginar(apiUrl) {
  let pagina = 1;
  let temMais = true;
  while (temMais) {
    const resposta = await fetch(`${apiUrl}?pagina=${pagina}`);
    const dados = await resposta.json();
    yield dados.itens;
    temMais = dados.temMais;
    pagina++;
  }
}

async function processarTudo() {
  for await (const itens of paginar('https://api.exemplo.com/usuarios')) {
    for (const item of itens) {
      console.log(item.nome);
    }
  }
}

Também é muito útil em interfaces de usuário em tempo real, como chats, feeds de notícias ou notificações. Quando você usa WebSockets ou Server-Sent Events, os dados chegam continuamente, e for await...of permite processar cada evento de forma simples e legível.

Além disso, a iteração assíncrona é a base para bibliotecas de processamento de dados, como RxJS, que oferecem operadores para transformar e combinar fluxos assíncronos. Embora RxJS use Observables, o conceito de consumir dados assíncronos de forma sequencial é o mesmo.

Em resumo, a iteração assíncrona é uma ferramenta versátil que simplifica o código e melhora a eficiência no tratamento de dados assíncronos, sendo indispensável para desenvolvedores JavaScript modernos.

Boas práticas

Ao usar for await...of, lembre-se de sempre envolver o código em uma função assíncrona, pois o for await só pode ser usado dentro de funções async. Além disso, trate erros adequadamente com try/catch, pois promessas rejeitadas durante a iteração podem interromper o loop. Considere também o cancelamento: se você precisar interromper a iteração antecipadamente, use break ou return, e verifique se o iterável assíncrono oferece um método de cancelamento (como return() no iterador) para liberar recursos.

Outra boa prática é evitar consumir streams diretamente com for await em ambientes onde a compatibilidade é limitada; verifique se o objeto é um async iterable ou use métodos como read() manualmente. E, ao criar seus próprios async iterables, sempre implemente o método return() para permitir uma limpeza adequada.

Referências

Exercícios

  1. Crie uma função geradora assíncrona que produza os números de 1 a 5 com um atraso de 200ms entre cada um. Use for await...of para iterar e imprimir cada número no console.

    ✓ Resposta:
    async function* geradorComAtraso() {
      for (let i = 1; i <= 5; i++) {
        await new Promise(resolve => setTimeout(resolve, 200));
        yield i;
      }
    }
    
    async function main() {
      for await (const numero of geradorComAtraso()) {
        console.log(numero);
      }
    }
    
    main();
    
  2. Implemente um objeto que seja um async iterable e que produza três strings: 'a', 'b', 'c', com um atraso de 100ms entre cada uma. Consuma-o com for await...of.

    ✓ Resposta:
    const asyncIterable = {
      [Symbol.asyncIterator]() {
        const valores = ['a', 'b', 'c'];
        let indice = 0;
        return {
          next() {
            if (indice < valores.length) {
              return new Promise(resolve => {
                setTimeout(() => resolve({ value: valores[indice++], done: false }), 100);
              });
            }
            return Promise.resolve({ done: true });
          }
        };
      }
    };
    
    async function consumir() {
      for await (const letra of asyncIterable) {
        console.log(letra);
      }
    }
    
    consumir();
    
  3. Usando Node.js, escreva um script que leia um arquivo de texto linha por linha (suponha que cada chunk seja uma linha) e imprima cada linha no console. Use fs.createReadStream e for await...of.

    ✓ Resposta:
    const fs = require('fs');
    
    async function lerLinhas(caminho) {
      const stream = fs.createReadStream(caminho, { encoding: 'utf8' });
      let buffer = '';
      for await (const chunk of stream) {
        buffer += chunk;
        let index;
        while ((index = buffer.indexOf('\n')) !== -1) {
          const linha = buffer.slice(0, index);
          console.log(linha);
          buffer = buffer.slice(index + 1);
        }
      }
      // Imprime a última linha se não terminar com \n
      if (buffer) console.log(buffer);
    }
    
    lerLinhas('arquivo.txt');
    
  4. Crie um gerador assíncrono que simule uma paginação de uma API fictícia. Ele deve receber um número total de páginas e produzir um array de itens para cada página. Use for await...of para processar todas as páginas e contar o total de itens.

    ✓ Resposta:
    async function* paginar(totalPaginas) {
      for (let pagina = 1; pagina <= totalPaginas; pagina++) {
        // Simula uma requisição assíncrona
        const itens = Array.from({ length: 10 }, (_, i) => `Item ${pagina}-${i + 1}`);
        await new Promise(resolve => setTimeout(resolve, 100));
        yield itens;
      }
    }
    
    async function processar() {
      let total = 0;
      for await (const itens of paginar(3)) {
        console.log('Processando página...');
        total += itens.length;
      }
      console.log(`Total de itens: ${total}`);
    }
    
    processar();
    
  5. Escreva uma função que receba um ReadableStream da Web e consuma todos os chunks usando for await...of, decodificando-os como texto UTF-8 e concatenando em uma string final. (Assuma que o stream já é um async iterable.)

    ✓ Resposta:
    async function lerStreamCompleto(stream) {
      let resultado = '';
      for await (const chunk of stream) {
        resultado += new TextDecoder().decode(chunk);
      }
      return resultado;
    }
    
    // Exemplo de uso:
    // const resposta = await fetch('https://exemplo.com')
    // const texto = await lerStreamCompleto(resposta.body)