Iteradores y Generadores Asíncronos en JavaScript
Los iteradores asíncronos son útiles cuando necesitamos manejar datos que llegan de manera asíncrona, como al descargar contenido en partes a través de una red. Los generadores asíncronos facilitan aún más este proceso.
Repaso de Iterables
Recordemos cómo funcionan los iterables en JavaScript. Supongamos que tenemos un objeto range
:
let range = {
from: 1,
to: 5
};
Queremos utilizar un bucle for..of
en este objeto para obtener valores del 1 al 5. Esto se puede lograr implementando un método especial llamado Symbol.iterator
.
Este método es invocado por el for..of
al iniciar el bucle y debe devolver un objeto con el método next
. Para cada iteración, se invoca el método next()
para obtener el siguiente valor. El next()
debe devolver un valor en el formato {done: true/false, value: <loop value>}
, donde done: true
indica el fin del bucle.
Aquí hay una implementación de range
como iterable:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() { // llamado una vez al inicio de for..of
return {
current: this.from,
last: this.to,
next() { // llamado en cada iteración para obtener el siguiente valor
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
for (let value of range) {
console.log(value); // 1 luego 2, luego 3, luego 4, luego 5
}
Iteradores Asíncronos
La iteración asíncrona es necesaria cuando los valores se obtienen de forma asíncrona, como después de un setTimeout
u otro tipo de retraso. Por ejemplo, un objeto puede necesitar realizar una solicitud de red para obtener el siguiente valor.
Para hacer un objeto iterable de manera asíncrona:
- Usa
Symbol.asyncIterator
en lugar deSymbol.iterator
. - El método
next()
debe devolver una promesa que se resuelva con el siguiente valor. - El método
next()
debe ser una funciónasync
para que podamos usarawait
dentro de él. - Utiliza
for await (let item of iterable)
para iterar sobre el objeto.
Como ejemplo inicial, hagamos que un objeto range
devuelva valores de manera asíncrona, uno por segundo.
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() {
return {
current: this.from,
last: this.to,
async next() {
await new Promise(resolve => setTimeout(resolve, 1000)); // espera 1 segundo
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
(async () => {
for await (let value of range) {
console.log(value); // 1, 2, 3, 4, 5 con un segundo de retraso entre cada uno
}
})();
Generadores Asíncronos
Para hacer que un objeto genere una secuencia de valores de manera asíncrona, usamos generadores asíncronos. La sintaxis es simple: anteponga async
a function*
. Esto hace que el generador sea asíncrono y permite usar await
dentro de él.
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 1000)); // espera 1 segundo
yield i;
}
}
(async () => {
for await (let value of generateSequence(1, 5)) {
console.log(value); // 1, 2, 3, 4, 5 con un segundo de retraso entre cada uno
}
})();
Diferencias Técnicas
En los generadores asíncronos, el método generator.next()
devuelve una promesa. En un generador normal, result = generator.next()
obtiene valores. En un generador asíncrono, necesitamos agregar await
así:
result = await generator.next(); // result = {value: ..., done: true/false}
Ejemplo del Mundo Real: Datos Paginados
Un caso de uso común es manejar datos paginados. Por ejemplo, GitHub devuelve commits en páginas de 30 elementos. Nuestra función fetchCommits(repo)
tomará los commits, manejando las solicitudes necesarias para obtener todas las páginas.
async function* fetchCommits(repo) {
let url = `https://api.github.com/repos/${repo}/commits`;
while (url) {
const response = await fetch(url, {
headers: { 'User-Agent': 'Our script' }
});
const body = await response.json();
let nextPage = response.headers.get('Link')?.match(/<(.*?)>; rel="next"/)?.[1];
url = nextPage;
for (let commit of body) {
yield commit;
}
}
}
(async () => {
let count = 0;
for await (const commit of fetchCommits('username/repository')) {
console.log(commit.author.login);
if (++count == 100) break;
}
})();
Resumen
Los iteradores y generadores normales son adecuados para datos que no tienen demoras en ser generados. Cuando los datos llegan de forma asíncrona, utilizamos iteradores y generadores asíncronos y for await..of
en lugar de for..of
.
Diferencias Sintácticas:
- Iteradores:
Symbol.iterator
vsSymbol.asyncIterator
next()
devuelve{value: ..., done: true/false}
vs unaPromise
que se resuelve con{value: ..., done: true/false}
- Generadores:
function*
vsasync function*
next()
devuelve{value: ..., done: true/false}
vs unaPromise
que se resuelve con{value: ..., done: true/false}
En el desarrollo web, a menudo tratamos con flujos de datos. Los generadores asíncronos nos permiten procesar estos datos de manera eficiente.