Funciones asíncronas: hacer promesas amigables

Las funciones asincrónicas te permiten escribir código basado en promesas como si fuera síncrono.

Las funciones asíncronas están habilitadas de forma predeterminada en Chrome, Edge, Firefox y Safari. son francamente maravillosas. Te permiten escribir código basado en promesas como si fuera síncrono, pero sin bloquear el subproceso principal. Hacen que tu código asíncrono menos “inteligente” sea más fácil de leer.

Las funciones asincrónicas funcionan de la siguiente manera:

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

Si usas la palabra clave async antes de una definición de función, puedes usar await dentro de la función. Cuando await una promesa, se pausa la función. de una manera sin bloqueos hasta que se establezca la promesa. Si la promesa se cumple, recuperar el valor. Si se rechaza la promesa, se arroja el valor rechazado.

Navegadores compatibles

Navegadores compatibles

  • Chrome: 55.
  • Límite: 15.
  • Firefox: 52.
  • Safari: 10.1.

Origen

Ejemplo: registra una recuperación

Supongamos que quieres recuperar una URL y registrar la respuesta como texto. Así se ve con promesas:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

Y aquí se ve lo mismo con el uso de funciones asincrónicas:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

Es la misma cantidad de líneas, pero desaparecen todas las devoluciones de llamada. Esto hace que sea mucho más fáciles de leer, en especial para quienes no están familiarizados con las promesas.

Valores de retorno asíncronos

Las funciones asíncronas siempre muestran una promesa, ya sea que uses await o no. Que resuelve con lo que muestre la función asíncrona, o rechaza con lo que arroje la función asíncrona. Entonces:

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

... llamar a hello() muestra una promesa que se cumple con "world".

async function foo() {
  await wait(500);
  throw Error('bar');
}

... llamar a foo() muestra una promesa que se rechaza con Error('bar').

Ejemplo: transmitir una respuesta

El beneficio de las funciones asincrónicas aumenta en ejemplos más complejos. Digamos que querías para transmitir una respuesta mientras cierras sesión en los fragmentos y devolver el tamaño final.

Aquí está con promesas:

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

Mira, Jake "poseedor de promesas" Archibald. Mira cómo llamo processResult() dentro de sí mismo para configurar un bucle asíncrono. Una escritura que generó Me siento muy inteligente. Pero como las empresas más "inteligentes" código, tienes que observarlo años para descubrir qué está haciendo, como una de esas fotos estilo ojo mágico de los años 90.

Intentémoslo de nuevo con funciones asíncronas:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

Todo lo "inteligente" ha desaparecido. El bucle asíncrono que me hizo tan presumido reemplaza por un confiable y aburrido bucle while. Mucho mejor. En el futuro, tendrás iteradores asíncronos, lo que reemplaza el bucle while por uno for-of para que sea aún más prolijo.

Otra sintaxis de función asíncrona

Ya te mostré async function() {}, pero la palabra clave async puede ser que se usa con otra sintaxis de funciones:

Funciones de flecha

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

Métodos de objeto

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then();

Métodos de clase

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then();

Debe tener cuidado. Evita generar demasiadas secuencias

Si bien estás escribiendo un código que luce síncrono, asegúrate de no perderte la oportunidad de hacer cosas en paralelo.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

Lo anterior tarda 1,000 ms en completarse, mientras que:

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

Lo anterior tarda 500 ms en completarse porque ambas esperas ocurren al mismo tiempo. Veamos un ejemplo práctico.

Ejemplo: muestra resultados de recuperaciones en orden

Supongamos que quieres recuperar una serie de URLs y registrarlas lo antes posible en la el orden correcto.

Respira profundo, así es como se ve eso con promesas:

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

Sí, así es, estoy usando reduce para encadenar una secuencia de promesas. Soy así que inteligentes. Pero esta es una codificación tan inteligente sin la cual es conveniente que no hagas nada.

Sin embargo, al convertir lo anterior en una función asíncrona, es tentador demasiado secuenciales:

No se recomienda: hay demasiadas secuencias
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Se ve mucho más prolija, pero mi segunda recuperación no comienza hasta que la primera leyeron por completo, etcétera. Esto es mucho más lento que el ejemplo de las promesas realiza las recuperaciones en paralelo. Afortunadamente, hay un punto medio ideal.
Recomendado: agradable y paralelo
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
En este ejemplo, las URLs se recuperan y se leen en paralelo, pero la parte Se reemplazó el bit de reduce por un bucle for estándar, aburrido y legible.

Solución de asistencia para el navegador: Generadores

Si orienta sus anuncios a navegadores que admiten generadores (que incluyen la versión más reciente de los navegadores principales ) puedes usar funciones asincrónicas de polyfill.

Babel lo hará por ti, este es un ejemplo mediante el REPL de Babel

Recomiendo el enfoque transpilado, porque puedes desactivarlo cuando los navegadores objetivo admiten funciones asincrónicas, pero si realmente no deseas utilizar una transpilador, puedes usar Polyfill de Babel y usarla tú mismo. En lugar de esta sintaxis:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

...incluirías el polyfill y escribe lo siguiente:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

Ten en cuenta que debes pasar un generador (function*) a createAsyncFunction, y usa yield en lugar de await. Aparte de eso, funciona de la misma manera.

Solución alternativa: regenerador

Si segmentas tus anuncios para navegadores más antiguos, Babel también puede transpilar generadores, lo que te permite utilizar funciones asincrónicas hasta IE8. Para hacer esto, necesitas Ajuste predeterminado de Babel para es2017 y el ajuste predeterminado es2015.

El resultado no es tan bonito, así que ten cuidado con un exceso de código.

¡Haz todo asincrónico!

Una vez que las funciones asincrónicas llegan a todos los navegadores, úsalas en cada una función que devuelve promesas. No solo hacen que tu código sea más prolijo, sino que también que la función siempre muestre una promesa.

Me entusiasmé mucho con las funciones asincrónicas en otra 2014 y es genial verlos si llegan, de verdad, en navegadores. ¡Uy!