Veamos Async/Await en JavaScript usando ejemplos

El autor del artículo examina ejemplos de Async/Await en JavaScript. En general, Async/Await es una forma conveniente de escribir código asincrónico. Antes de que apareciera esta función, dicho código se escribía mediante devoluciones de llamada y promesas. El autor del artículo original revela las ventajas de Async/Await analizando varios ejemplos.

Recordamos: para todos los lectores de "Habr": un descuento de 10 rublos al inscribirse en cualquier curso de Skillbox utilizando el código promocional "Habr".

Skillbox recomienda: Curso educativo en línea "Desarrollador Java".

Devolución de llamada

La devolución de llamada es una función cuya llamada se retrasa indefinidamente. Anteriormente, las devoluciones de llamada se usaban en aquellas áreas del código donde el resultado no se podía obtener de inmediato.

A continuación se muestra un ejemplo de lectura asincrónica de un archivo en Node.js:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

Los problemas surgen cuando es necesario realizar varias operaciones asincrónicas a la vez. Imaginemos este escenario: se realiza una solicitud a la base de datos de usuarios de Arfat, es necesario leer su campo perfil_img_url y descargar una imagen del servidor someserver.com.
Después de la descarga, convertimos la imagen a otro formato, por ejemplo de PNG a JPEG. Si la conversión fue exitosa, se envía una carta al correo electrónico del usuario. A continuación, se ingresa información sobre el evento en el archivo transformaciones.log, indicando la fecha.

Vale la pena prestar atención a la superposición de devoluciones de llamada y la gran cantidad de }) en la parte final del código. Se llama Callback Hell o Pyramid of Doom.

Las desventajas de este método son obvias:

  • Este código es difícil de leer.
  • También es difícil manejar errores, lo que a menudo conduce a una mala calidad del código.

Para resolver este problema, se agregaron promesas a JavaScript. Le permiten reemplazar el anidamiento profundo de devoluciones de llamadas con la palabra .entonces.

El aspecto positivo de las promesas es que hacen que el código sea mucho mejor legible, de arriba a abajo en lugar de de izquierda a derecha. Sin embargo, las promesas también tienen sus problemas:

  • Necesitas agregar muchos .luego.
  • En lugar de try/catch, se utiliza .catch para manejar todos los errores.
  • Trabajar con múltiples promesas dentro de un ciclo no siempre es conveniente; en algunos casos, complican el código.

Aquí hay un problema que mostrará el significado del último punto.

Supongamos que tenemos un bucle for que imprime una secuencia de números del 0 al 10 en intervalos aleatorios (0–n segundos). Al utilizar promesas, debe cambiar este bucle para que los números se impriman en secuencia del 0 al 10. Entonces, si se necesitan 6 segundos para imprimir un cero y 2 segundos para imprimir un uno, el cero debe imprimirse primero y luego Comenzará la cuenta regresiva para imprimir el uno.

Y, por supuesto, no utilizamos Async/Await o .sort para resolver este problema. Al final hay un ejemplo de solución.

Funciones asíncronas

La adición de funciones asíncronas en ES2017 (ES8) simplificó la tarea de trabajar con promesas. Observo que las funciones asíncronas funcionan "además" de las promesas. Estas funciones no representan conceptos cualitativamente diferentes. Las funciones asíncronas están pensadas como una alternativa al código que utiliza promesas.

Async/Await permite organizar el trabajo con código asincrónico en un estilo sincrónico.

Por lo tanto, conocer las promesas facilita la comprensión de los principios de Async/Await.

sintaxis

Normalmente consta de dos palabras clave: async y await. La primera palabra convierte la función en asincrónica. Estas funciones permiten el uso de await. En cualquier otro caso, utilizar esta función generará un error.

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async se inserta al principio de la declaración de función y, en el caso de una función de flecha, entre el signo "=" y los paréntesis.

Estas funciones pueden colocarse en un objeto como métodos o usarse en una declaración de clase.

// As an object's method
 
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
 
// In a class
 
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

¡NÓTESE BIEN! Vale la pena recordar que los constructores de clases y los captadores/definidores no pueden ser asincrónicos.

Semántica y reglas de ejecución.

Las funciones asíncronas son básicamente similares a las funciones JS estándar, pero existen excepciones.

Por tanto, las funciones asíncronas siempre devuelven promesas:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

Específicamente, fn devuelve la cadena hola. Bueno, dado que se trata de una función asincrónica, el valor de la cadena se envuelve en una promesa utilizando un constructor.

Aquí hay un diseño alternativo sin Async:

function fn() {
  return Promise.resolve('hello');
}
 
fn().then(console.log);
// hello

En este caso, la promesa se devuelve "manualmente". Una función asincrónica siempre está envuelta en una nueva promesa.

Si el valor de retorno es primitivo, la función asíncrona devuelve el valor envolviéndolo en una promesa. Si el valor devuelto es un objeto de promesa, su resolución se devuelve en una nueva promesa.

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

Pero ¿qué pasa si hay un error dentro de una función asincrónica?

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

Si no se procesa, foo() devolverá una promesa con rechazo. En esta situación, se devolverá Promise.reject que contiene un error en lugar de Promise.resolve.

Las funciones asíncronas siempre generan una promesa, independientemente de lo que se devuelva.

Las funciones asincrónicas se detienen en cada espera.

Await afecta las expresiones. Entonces, si la expresión es una promesa, la función asíncrona se suspende hasta que se cumpla la promesa. Si la expresión no es una promesa, se convierte en una promesa a través de Promise.resolve y luego se completa.

// utility function to cause delay
// and get random value
 
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
 
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
 
  return a + b * c;
}
 
// Execute fn
fn().then(console.log);

Y aquí hay una descripción de cómo funciona la función fn.

  • Después de llamarlo, la primera línea se convierte de const a = await 9; en const a = await Promise.resolve(9);.
  • Después de usar Await, la ejecución de la función se suspende hasta que a obtenga su valor (en la situación actual es 9).
  • delayAndGetRandom(1000) pausa la ejecución de la función fn hasta que se completa (después de 1 segundo). Esto detiene efectivamente la función fn durante 1 segundo.
  • delayAndGetRandom(1000) a través de resolve devuelve un valor aleatorio, que luego se asigna a la variable b.
  • Bueno, el caso de la variable c es similar al caso de la variable a. Después de eso, todo se detiene por un segundo, pero ahora delayAndGetRandom(1000) no devuelve nada porque no es necesario.
  • Como resultado, los valores se calculan mediante la fórmula a + b * c. El resultado está envuelto en una promesa usando Promise.resolve y devuelto por la función.

Estas pausas pueden recordar a los generadores en ES6, pero hay algo en ello. tus razones.

Resolviendo el problema

Bueno, ahora veamos la solución al problema mencionado anteriormente.

La función FinishMyTask usa Await para esperar los resultados de operaciones como queryDatabase, sendEmail, logTaskInFile y otras. Si compara esta solución con aquella en la que se utilizaron promesas, las similitudes serán obvias. Sin embargo, la versión Async/Await simplifica enormemente todas las complejidades sintácticas. En este caso, no hay una gran cantidad de devoluciones de llamada y cadenas como .then/.catch.

Aquí hay una solución con la salida de números, hay dos opciones.

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
 
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});
 
// Implementation Two (Using Recursion)
 
const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {
 
    if (i === 10) {
      return undefined;
    }
 
    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

Y aquí hay una solución que utiliza funciones asíncronas.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

Manejo de errores

Los errores no controlados están envueltos en una promesa rechazada. Sin embargo, las funciones asíncronas pueden usar try/catch para manejar errores sincrónicamente.

async function canRejectOrReturn() {
  // wait one second
  await new Promise(res => setTimeout(res, 1000));
 
// Reject with ~50% probability
  if (Math.random() > 0.5) {
    throw new Error('Sorry, number too big.')
  }
 
return 'perfect number';
}

canRejectOrReturn() es una función asincrónica que tiene éxito (“número perfecto”) o falla con un error (“Lo siento, el número es demasiado grande”).

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Dado que el ejemplo anterior espera que se ejecute canRejectOrReturn, su propia falla resultará en la ejecución del bloque catch. Como resultado, la función foo terminará con indefinido (cuando no se devuelve nada en el bloque try) o con un error detectado. Como resultado, esta función no fallará porque try/catch manejará la función foo en sí.

Aquí hay otro ejemplo:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Vale la pena prestar atención al hecho de que en el ejemplo, canRejectOrReturn se devuelve desde foo. En este caso, Foo termina con un número perfecto o devuelve un error (“Lo siento, el número es demasiado grande”). El bloque catch nunca se ejecutará.

El problema es que foo devuelve la promesa pasada desde canRejectOrReturn. Entonces la solución a foo se convierte en la solución a canRejectOrReturn. En este caso, el código constará de sólo dos líneas:

try {
    const promise = canRejectOrReturn();
    return promise;
}

Esto es lo que sucede si usa await y return juntos:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

En el código anterior, foo saldrá exitosamente con un número perfecto y un error detectado. Aquí no habrá rechazos. Pero foo volverá con canRejectOrReturn, no con indefinido. Asegurémonos de esto eliminando la línea return await canRejectOrReturn():

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

Errores y trampas comunes

En algunos casos, el uso de Async/Await puede provocar errores.

espera olvidada

Esto sucede con bastante frecuencia: la palabra clave await se olvida antes de la promesa:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

Como puede ver, no hay espera ni retorno en el código. Por lo tanto, foo siempre sale con indefinido sin un retraso de 1 segundo. Pero la promesa se cumplirá. Si arroja un error o un rechazo, se llamará a UnhandledPromiseRejectionWarning.

Funciones asíncronas en devoluciones de llamada

Las funciones asíncronas se utilizan con bastante frecuencia en .map o .filter como devoluciones de llamada. Un ejemplo es la función fetchPublicReposCount(nombre de usuario), que devuelve el número de repositorios abiertos en GitHub. Digamos que hay tres usuarios cuyas métricas necesitamos. Aquí está el código para esta tarea:

const url = 'https://api.github.com/users';
 
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

Necesitamos cuentas de ArfatSalman, octocat y norvig. En este caso hacemos:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Vale la pena prestar atención a Await en la devolución de llamada .map. Aquí, count es una serie de promesas y .map es una devolución de llamada anónima para cada usuario especificado.

Uso demasiado consistente de await

Tomemos este código como ejemplo:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

Aquí el número de repositorio se coloca en la variable de conteo, luego este número se agrega a la matriz de conteos. El problema con el código es que hasta que lleguen los datos del primer usuario del servidor, todos los usuarios posteriores estarán en modo de espera. Por tanto, sólo se procesa un usuario a la vez.

Si, por ejemplo, se necesitan unos 300 ms para procesar a un usuario, entonces para todos los usuarios ya es un segundo; el tiempo invertido depende linealmente del número de usuarios. Pero como la obtención del número de repositorios no depende uno del otro, los procesos se pueden paralelizar. Esto requiere trabajar con .map y Promise.all:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all recibe una serie de promesas como entrada y devuelve una promesa. Este último, después de que se hayan completado todas las promesas en la matriz o en el primer rechazo, se completa. Puede suceder que no todos se inicien al mismo tiempo; para garantizar un inicio simultáneo, puede utilizar p-map.

Conclusión

Las funciones asíncronas son cada vez más importantes para el desarrollo. Bueno, para el uso adaptativo de funciones asíncronas, deberías usar Iteradores asíncronos. Un desarrollador de JavaScript debería estar bien versado en esto.

Skillbox recomienda:

Fuente: habr.com

Añadir un comentario