preloader

Iteradores y Generadores Asíncronos en JavaScript

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:

  1. Usa Symbol.asyncIterator en lugar de Symbol.iterator.
  2. El método next() debe devolver una promesa que se resuelva con el siguiente valor.
  3. El método next() debe ser una función async para que podamos usar await dentro de él.
  4. 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 vs Symbol.asyncIterator
    • next() devuelve {value: ..., done: true/false} vs una Promise que se resuelve con {value: ..., done: true/false}
  • Generadores:
    • function* vs async function*
    • next() devuelve {value: ..., done: true/false} vs una Promise 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.

Related Post

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *