Iteradores Assíncronos em JavaScript
Iteradores assíncronos combinam as capacidades de iteradores com os operadores async
e await
. Eles são projetados principalmente para acessar fontes de dados que utilizam APIs assíncronas. Isso inclui dados carregados em partes, como da rede, do sistema de arquivos ou de um banco de dados.
Do tema sobre iteradores, sabemos que um iterador fornece o método next()
, que retorna um objeto com duas propriedades: { value, done }
. A propriedade value
contém um valor, que pode ser obtido em um loop for..of
ao iterar sobre o objeto. A propriedade done
indica se a iteração foi concluída. Se done
for false
, significa que o iterador ainda não terminou e há mais objetos disponíveis. Se done
for true
, a iteração está concluída e não há mais objetos para iterar.
Um iterador assíncrono é semelhante a um iterador síncrono, exceto que seu método next()
retorna um objeto Promise
. A Promise
retorna um objeto { value, done }
.
Loop for-await-of
Para obter dados usando iteradores assíncronos, usamos o loop for-await-of
:
for await (variable of iterable) {
// ações
}
No loop for-await-of
, após o operador of
vem um conjunto de dados que pode ser iterado. Isso pode ser uma fonte de dados assíncrona, mas também pode ser uma fonte de dados síncrona, como arrays ou objetos embutidos como String
, Map
, Set
, etc.
Vale notar que esta forma de loop só pode ser usada em funções definidas com o operador async
.
Vejamos um exemplo simples onde a fonte de dados é um array comum:
const dataSource = ["Tom", "Sam", "Bob"];
async function readData() {
for await (const item of dataSource) {
console.log(item);
}
}
readData();
// Tom
// Sam
// Bob
Aqui, o array dataSource
é iterado. Durante o loop, para a fonte de dados (neste caso, o array), um iterador assíncrono é criado implicitamente usando o método [Symbol.asyncIterator]()
. Cada vez que acessamos um elemento, um objeto Promise
é retornado pelo iterador, e obtemos o elemento atual do array.
Criando um Iterador Assíncrono
No exemplo acima, o iterador assíncrono foi criado implicitamente. No entanto, podemos defini-lo explicitamente. Por exemplo, vamos definir um iterador assíncrono que retorna elementos de um array:
const generatePerson = {
[Symbol.asyncIterator]() {
return {
index: 0,
people: ["Tom", "Sam", "Bob"],
next() {
if (this.index < this.people.length) {
return Promise.resolve({ value: this.people[this.index++], done: false });
}
return Promise.resolve({ done: true });
},
};
},
};
Aqui, definimos o objeto generatePerson
, que possui o método [Symbol.asyncIterator]()
, representando o iterador assíncrono. A implementação permite que o objeto generatePerson
seja iterável.
Pontos principais de um iterador assíncrono:
- Um iterador assíncrono é implementado com o método
[Symbol.asyncIterator]()
que retorna um objeto. - O objeto retornado pelo iterador possui o método
next()
que retorna um objetoPromise
. - O objeto
Promise
retorna um objeto com duas propriedades{ value, done }
. A propriedadevalue
contém um valor, enquantodone
indica se a iteração foi concluída. Sedone
fortrue
, não há mais objetos para iterar, evalue
não precisa ser especificado.
No exemplo, o iterador retorna usuários sequencialmente. Para armazenar os usuários, usamos um array people
e uma variável index
para o índice atual.
index: 0,
people: ["Tom", "Sam", "Bob"],
No método next()
, retornamos um objeto Promise
. Se o índice atual for menor que o tamanho do array, retornamos um Promise
com o elemento do array no índice atual:
return Promise.resolve({ value: this.people[this.index++], done: false });
Se todos os elementos do array foram obtidos, retornamos um Promise
com { done: true }
:
Isso indica ao código externo que todos os valores do iterador já foram obtidos.
Agora, vejamos como obter dados do iterador:
Podemos acessar o próprio iterador assíncrono:
generatePerson[Symbol.asyncIterator](); // obtenha o iterador
assíncrono;
E chamar explicitamente seu método next()
:
generatePerson[Symbol.asyncIterator]().next(); // Promise
Este método retorna uma Promise
, na qual podemos chamar o método then()
e processar seu valor:
generatePerson[Symbol.asyncIterator]()
.next()
.then((data) => console.log(data)); // {value: "Tom", done: false}
O objeto retornado pela Promise
possui as propriedades value
e done
. Podemos acessar o valor diretamente:
generatePerson[Symbol.asyncIterator]()
.next()
.then((data) => console.log(data.value)); // Tom
Como o método next()
retorna uma Promise
, podemos usar o operador await
para obter os valores:
async function printPeople() {
const peopleIterator = generatePerson[Symbol.asyncIterator]();
let personData;
while (!(personData = await peopleIterator.next()).done) {
console.log(personData.value);
}
}
printPeople();
Aqui, em uma função assíncrona com um loop while
, usamos await
para obter sequencialmente os objetos Promise
do iterador, extraindo dados até alcançar o fim.
No entanto, para iterar sobre um objeto de iterador assíncrono, é mais simples usar o loop for-await-of
:
const generatePerson = {
people: ["Tom", "Sam", "Bob"],
[Symbol.asyncIterator]() {
let index = 0;
return {
next: async () => {
if (index < this.people.length) {
// Simulando um atraso assíncrono de 2 segundos para cada pessoa
const value = await new Promise((resolve) => setTimeout(() => resolve(this.people[index++]), 2000));
return { value, done: false };
}
return { done: true };
},
};
},
};
async function printPeople() {
for await (const person of generatePerson) {
console.log(person);
}
}
printPeople();
Como o objeto generatePerson
implementa o método [Symbol.asyncIterator]()
, podemos iterar sobre ele com o loop for-await-of
. Em cada iteração, o método next()
retorna uma Promise
com o próximo elemento do array people. O resultado será:
Tom Sam Bob
Vale ressaltar que não podemos usar um loop for-of
comum para iterar sobre um objeto com um iterador assíncrono.
Outro exemplo simples é a obtenção de números:
const generateNumber = {
[Symbol.asyncIterator]() {
return {
current: 0,
end: 10,
next() {
if (this.current <= this.end) {
return Promise.resolve({ value: this.current++, done: false });
}
return Promise.resolve({ done: true });
},
};
},
};
async function printNumbers() {
for await (const n of generateNumber) {
console.log(n);
}
}
printNumbers();
Neste exemplo, o iterador assíncrono do objeto generateNumber
retorna números de 0 a 10.
Conclusão
Iteradores assíncronos são uma maneira eficaz de acessar fontes de dados assíncronas. Eles combinam as capacidades de iteradores com os operadores async
e await
. Isso permite que você acesse dados carregados em partes, como da rede, do sistema de arquivos ou de um banco de dados.