Analizando Async / Await en JavaScript con ejemplos



El autor del artículo analiza Async / Await en JavaScript usando ejemplos. En general, Async / Await es una forma conveniente de escribir código asincrónico. Antes de esta oportunidad, se escribió un código similar utilizando devoluciones de llamada y promesas. El autor del artículo original revela los beneficios de Async / Await al examinar varios ejemplos.

Le recordamos: para todos los lectores de "Habr": un descuento de 10.000 rublos al registrarse en cualquier curso de Skillbox con el código de promoción "Habr".

Skillbox recomienda: El curso de educación en línea para desarrolladores de 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 partes del código donde el resultado no se podía obtener de inmediato.

Aquí hay un ejemplo de lectura asíncrona de un archivo en Node.js:

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

Los problemas surgen cuando necesita realizar varias operaciones asincrónicas a la vez. Imaginemos este escenario: se realiza una solicitud a la base de datos de usuarios de Arfat, debe leer su campo profile_img_url y descargar una imagen del servidor someserver.com.
Después de la descarga, convierta la imagen a otro formato, por ejemplo, de PNG a JPEG. Si la conversión fue exitosa, se envía un correo electrónico al correo del usuario. Además, la información sobre el evento se ingresa en el archivo transformations.log con la fecha.



Vale la pena prestar atención a la imposición de devoluciones de llamada y un gran número}) en la parte final del código. Esto 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 los errores, lo que a menudo conduce a un deterioro en la calidad del código.

Para resolver este problema, se agregaron promesas a JavaScript. Le permiten reemplazar la anidación profunda de las devoluciones de llamada con la palabra .then.



El punto positivo de las promesas fue que con ellos el código se lee mucho mejor, de arriba a abajo, y no de izquierda a derecha. Sin embargo, las promesas también tienen sus problemas:

  • Necesita agregar una gran cantidad de .then.
  • En lugar de try / catch, .catch se usa para manejar todos los errores.
  • Trabajar con varias promesas dentro de un ciclo dista mucho de ser siempre conveniente; en algunos casos, complican el código.

Aquí hay una tarea que muestra el significado del último elemento.

Supongamos que hay un ciclo for que imprime una secuencia de números del 0 al 10 con un intervalo aleatorio (0 - n segundos). Usando promesas, debe cambiar este ciclo para que los números se muestren en la secuencia de 0 a 10. Por lo tanto, si la salida cero tarda 6 segundos y las unidades tardan 2 segundos, primero debe salir cero, y luego comenzará la cuenta regresiva de salida de la unidad.

Y, por supuesto, para resolver este problema, no utilizamos Async / Await o .sort. Un ejemplo de una solución está al final.

Funciones asincrónicas


Agregar funciones asíncronas a ES2017 (ES8) ha simplificado la tarea de trabajar con promesas. Observo que las funciones asíncronas funcionan por encima de las promesas. Estas funciones no representan conceptos cualitativamente diferentes. Las funciones asíncronas fueron concebidas como una alternativa al código que usa promesas.

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

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

Sintaxis

En una situación típica, consta de dos palabras clave: asíncrono y esperar. La primera palabra hace que la función sea asíncrona. Estas funciones permiten esperar. En cualquier otro caso, el uso de esta función causará 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 comienzo de la declaración de la función, y en el caso de la función de flecha, entre el signo "=" y los corchetes.

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'); } } 

NB! Vale la pena recordar que los constructores de clases y getters / setters no pueden ser asíncronos.

Semántica y reglas de ejecución.

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

Entonces, las funciones asíncronas siempre devuelven promesas:

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

En particular, fn devuelve la cadena hola. Bueno, dado que esta es una función asincrónica, el valor de la cadena está envuelto en una promesa usando el constructor.

Aquí hay un diseño alternativo sin Async:

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

En este caso, el retorno de la promesa se realiza "manualmente". Una función asincrónica siempre se envuelve en una nueva promesa.

En el caso de que el valor de retorno sea primitivo, la función asíncrona devuelve un valor, envolviéndolo en una promesa. En el caso de que el valor de retorno sea el objeto de la promesa, su solución se devuelve en la nueva promesa.

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

Pero, ¿qué sucede si se produce un error dentro de la función asincrónica?

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

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

Las funciones asíncronas en la salida siempre dan promesas, independientemente de lo que se devuelva.

Las funciones asincrónicas se pausan en cada espera.

Esperar afecta las expresiones. Entonces, si la expresión es una promesa, la función asíncrona se suspende hasta que se ejecute la promesa. En el caso de que la expresión no sea una promesa, se convierte en una promesa a través de Promise.resolve y luego termina.

 // 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); 

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 = waitit 9; en const a = wait Promise.resolve (9);.
  • Después de usar Await, la ejecución de la función se suspende hasta que recibe su valor (en la situación actual, es 9).
  • delayAndGetRandom (1000) detiene la ejecución de la función fn hasta que finaliza (después de 1 segundo). En realidad, esto detiene la función fn durante 1 segundo.
  • delayAndGetRandom (1000) a través de resolver 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, ya que esto no es obligatorio.
  • 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 parecerse a los generadores en ES6, pero hay razones para esto .

Resolvemos el problema


Bueno, ahora veamos la solución al problema que se mencionó anteriormente.



La función finishMyTask usa Aguardar para esperar los resultados de operaciones como queryDatabase, sendEmail, logTaskInFile y otros. Si comparamos esta decisión con el lugar donde se usaron las promesas, las similitudes se harán evidentes. Sin embargo, la versión con Async / Await simplifica enormemente todas las dificultades sintácticas. En este caso, no hay muchas 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 procesados ​​se envuelven en promesas rechazadas. Sin embargo, en las funciones asíncronas, puede usar la construcción try / catch para realizar el manejo de errores sincrónicos.

 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, número demasiado grande").

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

Como se espera que canRejectOrReturn se ejecute en el ejemplo anterior, su propia terminación fallida implicará la ejecución del bloque catch. Como resultado, la función foo finalizará con indefinido (cuando no se devuelve nada en el bloque try) o con un error detectado. Como resultado, esta función no fallará, ya que 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 de foo canRejectOrReturn se devuelve. En este caso, Foo se completa con un número perfecto o devuelve un error de error ("Lo siento, el número es demasiado grande"). El bloque catch nunca se ejecutará.

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

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

Pero, ¿qué sucede si usas esperar y volver juntos?

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

En el código anterior, foo tiene éxito con el número perfecto y el error detectado. No habrá fallas. Pero foo terminará con canRejectOrReturn, y no con undefined. Asegurémonos de esto eliminando el retorno de la línea canRejectOrReturn ():

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

Errores comunes y trampas


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

Olvidado espera

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

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

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

Funciones asíncronas en devoluciones de llamada

Las funciones asíncronas a menudo se usan 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 ArfatSalman, octocat, norvig. En este caso, ejecute:

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

Debe prestar atención a Aguardar en la devolución de llamada .map. Aquí cuenta una serie de promesas, bueno .map es una devolución de llamada anónima para cada usuario especificado.

Uso excesivamente consistente de esperar

Tome el siguiente 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 conteo 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 primeros datos de usuario del servidor, todos los usuarios posteriores estarán en modo de espera. Por lo tanto, en un solo momento, solo se procesa un usuario.

Si, por ejemplo, se requieren aproximadamente 300 ms para procesar un usuario, entonces para todos los usuarios esto ya es un segundo, el tiempo empleado linealmente depende del número de usuarios. Pero dado que obtener el número de repos no depende el uno del otro, los procesos pueden ser paralelos. 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 en la entrada recibe una serie de promesas con el regreso de la promesa. El último después de completar todas las promesas en la matriz o en el primer redject se completa. Puede suceder que no todos se inicien al mismo tiempo; para garantizar el lanzamiento simultáneo, puede usar p-map.

Conclusión


Las funciones asíncronas son cada vez más importantes para el desarrollo. Bueno, para el uso adaptativo de las funciones asíncronas, vale la pena usar los iteradores asíncronos. El desarrollador de JavaScript debería estar bien versado en esto.

Skillbox recomienda:

Source: https://habr.com/ru/post/458950/


All Articles