El autor del artículo analiza con ejemplos Async/Await en JavaScript. En general, Async/Await es una forma conveniente de escribir código asíncrono. Antes de que existiera esta posibilidad, dicho código se escribía utilizando callbacks y promesas. El autor del artículo original revela las ventajas de Async/Await, analizando diferentes ejemplos.
Recordamos: para todos los lectores de «Habr» — un descuento de 10,000 rublos al registrarse en cualquier curso de Skillbox usando el código promocional «Habr».
Skillbox recomienda: Curso en línea educativo .
Callback
Un callback es una función cuyo llamado se retrasa indefinidamente. Anteriormente, los callbacks se utilizaban en aquellas partes del código donde el resultado no podía obtenerse 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 se necesita realizar varias operaciones asíncronas al mismo tiempo. Imaginemos el siguiente escenario: se realiza una consulta a la base de datos del usuario Arfat, se necesita leer su campo profile_img_url y cargar la imagen desde el servidor someserver.com.
Después de cargar, convertimos la imagen a otro formato, por ejemplo, de PNG a JPEG. Si la conversión es exitosa, se envía un correo electrónico al usuario. Luego, la información sobre el evento se registra en el archivo transformations.log con la fecha indicada.
Es importante señalar la superposición de callbacks y la gran cantidad de }) en la parte final del código. Esto se llama Callback Hell o Pirámide de la Muerte.
Las desventajas de este enfoque son evidentes:
- Este código es difícil de leer.
- También es complicado manejar errores, lo que a menudo conduce a una disminución de la calidad del código.
Para resolver este problema, se añadieron promesas en JavaScript. Estas permiten reemplazar la profunda anidación de callbacks con la palabra .then.
Una ventaja de las promesas es que con ellas el código se lee mucho mejor, de arriba hacia abajo, y no de izquierda a derecha. Sin embargo, las promesas también tienen sus propios problemas:
- Es necesario agregar muchas .then.
- En lugar de try/catch se utiliza .catch para manejar todos los errores.
- Trabajar con varias promesas dentro de un solo ciclo no siempre es conveniente; en algunos casos, complican el código.
Aquí hay un problema que mostrará la importancia del último punto.
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, debemos modificar este ciclo para que los números se impriman en orden del 0 al 10. Así, si la impresión del cero toma 6 segundos y la del uno 2 segundos, primero se debe imprimir el cero y luego comenzará el conteo para imprimir el uno.
Y, por supuesto, para resolver este problema no utilizamos Async/Await ni .sort. Un ejemplo de solución se encuentra al final.
Funciones async
La adición de funciones async en ES2017 (ES8) facilitó el trabajo con promesas. Cabe destacar que las funciones async funcionan 'sobre' las promesas. Estas funciones no representan conceptos fundamentalmente diferentes. Las funciones async 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.
Así, el conocimiento de promesas facilita la comprensión de los principios de Async/Await.
Sintaxis
En condiciones normales, consta de dos palabras clave: async y await. La primera convierte la función en asincrónica. En tales funciones, se permite el uso de await. En cualquier otro caso, el uso de 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 la función, y en el caso de usar una función de flecha, entre el signo '=' y los paréntesis.
Estas funciones se pueden colocar en un objeto como métodos o usarse en la declaración de una 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');
}
}¡Atención! Es importante recordar que los constructores de clases y los getters/setters no pueden ser asincrónicos.
Semántica y reglas de ejecución
Las funciones async, en principio, son similares a las funciones estándar de JS, pero hay excepciones.
Así, las funciones async siempre devuelven promesas:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloEn particular, fn devuelve la cadena hello. Y como es una función asincrónica, el valor de la cadena se envuelve en una promesa mediante el constructor.
Aquí hay una construcción alternativa sin Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloEn este caso, la devolución 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 devuelto sea un primitivo, la función async devuelve el valor envolviéndolo en una promesa. Si el valor devuelto es un objeto 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é sucede si hay un error dentro de la función asincrónica?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Si no se maneja, foo() devolverá una promesa con un rechazo. En esta situación, en lugar de Promise.resolve se devolverá Promise.reject, que contendrá el error.
Las funciones async siempre devuelven una promesa, independientemente de lo que se devuelva.
Las funciones asíncronas se pausan en cada await.
Await afecta a las expresiones. Así, si la expresión es una promesa, la función async se pausa hasta que se ejecute la promesa. Si la expresión no es una promesa, se convierte en 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);Aquí hay una descripción de cómo funciona la función fn.
- Después de su llamada, 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 pausa hasta que a obtiene su valor (en este caso, 9).
- delayAndGetRandom(1000) pausa la ejecución de la función fn hasta que esta se complete (después de 1 segundo). Esto es, de hecho, detener la función fn durante 1 segundo.
- delayAndGetRandom(1000) devuelve un valor aleatorio a través de resolve, que luego se asigna a la variable b.
- En cuanto a la variable c, es análoga a la variable a. Después de esto, todo se detiene durante un segundo, pero ahora delayAndGetRandom(1000) no devuelve nada, ya que no es necesario.
- Al final, los valores se consideran según la fórmula a + b * c. El resultado se envuelve en una promesa mediante Promise.resolve y se devuelve por la función.
Estas pausas pueden recordar a los generadores en ES6, pero hay .
Resolviendo el problema
Ahora, veamos la solución al problema mencionado anteriormente.
En la función finishMyTask se utiliza Await para esperar los resultados de operaciones como queryDatabase, sendEmail, logTaskInFile entre otras. Si comparamos esta solución con la que utiliza promesas, la similitud es evidente. Sin embargo, la versión con Async/Await simplifica considerablemente todas las complejidades sintácticas. En este caso, no hay una gran cantidad de callbacks ni cadenas como .then/.catch.
Aquí hay una solución para imprimir números, donde hay dos variantes.
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í está la solución usando funciones async.
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 manejados se envuelven en una promesa rechazada. Sin embargo, en las funciones async se puede utilizar la estructura try/catch para realizar el manejo de errores de manera sincrónica.
async function canRejectOrReturn() {
// esperar un segundo
await new Promise(res => setTimeout(res, 1000));
// Rechazar con un ~50% de probabilidad
if (Math.random() > 0.5) {
throw new Error('Lo siento, el número es demasiado grande.')
}
return 'número perfecto';
}canRejectOrReturn() es una función asíncrona que finaliza exitosamente (‘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 capturado';
}
}Dado que en el ejemplo anterior se espera la ejecución de canRejectOrReturn, una falla propia resultará en la ejecución del bloque catch. Como resultado, la función foo finalizará ya sea con undefined (cuando no se retorna nada dentro del bloque try), o con error capturado. Por lo tanto, esta función no tendrá un fallo ya que el try/catch se encarga del manejo de la propia función foo.
Aquí hay otro ejemplo:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error capturado';
}
}Vale la pena señalar que en el ejemplo de foo se retorna canRejectOrReturn. En este caso, foo finaliza ya sea con número perfecto, o al menos con el error Error (‘Lo siento, el número es demasiado grande’). El bloque catch nunca se ejecutará.
El problema es que foo retorna la promesa pasada de canRejectOrReturn. Por lo tanto, la resolución de la función foo se convierte en la resolución de canRejectOrReturn. En este caso, el código consistirá en solo dos líneas:
try {
const promise = canRejectOrReturn();
return promise;
}Y esto es lo que sucederá si se utilizan juntos await y return:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error capturado';
}
}En el código anterior, foo terminará con éxito tanto con un número perfecto como con un error capturado. Aquí no habrá fallos. Pero foo terminará con canRejectOrReturn, en lugar de undefined. Asegurémonos de esto, eliminando la línea return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Errores comunes y trampas
En algunos casos, el uso de Async/Await puede llevar a errores.
Await olvidado
Esto ocurre con bastante frecuencia: se olvida la palabra clave await antes de una promesa:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}En el código, como se puede ver, no hay ni await ni return. Por lo tanto, foo siempre termina con undefined sin una demora de 1 segundo. Sin embargo, la promesa se ejecutará. Si da un error o se rechaza, se activará UnhandledPromiseRejectionWarning.
Funciones Async en callbacks
Las funciones Async se utilizan con bastante frecuencia en .map o .filter como callbacks. Un ejemplo es la función fetchPublicReposCount(username), que devuelve la cantidad de repositorios públicos en GitHub. Supongamos que hay tres usuarios cuyos datos necesitamos. Aquí está el código para esta tarea:
const url = 'https://api.github.com/users';
// Función utilitaria para obtener los conteos de repositorios
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Necesitamos las cuentas ArfatSalman, octocat, norvig. En este caso, ejecutamos:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Es importante notar el Await en el callback .map. Aquí, counts es un array de promesas, mientras que .map es un callback anónimo para cada usuario especificado.
Uso excesivo de await secuencialmente
Como ejemplo, tomemos este código:
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í, en la variable count se almacena el número de repositorios, y luego ese número se añade al array counts. El problema del código es que, mientras no lleguen los datos del primer usuario, todos los siguientes usuarios quedarán en modo de espera. Así, en un momento dado, solo se procesa a un usuario.
Si, por ejemplo, el procesamiento de un usuario toma alrededor de 300 ms, para todos los usuarios ya se trata de un segundo; el tiempo consumido depende linealmente del número de usuarios. Pero dado que la obtención de la cantidad de repositorios no depende unos de otros, los procesos se pueden paralelizar. Para esto, es necesario 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 un arreglo de promesas que devuelve otra promesa. Esta última se completa una vez que todas las promesas en el arreglo se han resuelto o al ser la primera en rechazar. Puede ocurrir que no todas se inicien al mismo tiempo; para asegurar un inicio simultáneo, se puede utilizar p-map.
Conclusión
Las funciones asíncronas se están volviendo cada vez más importantes para el desarrollo. Para un uso adaptable de las funciones asíncronas, es recomendable utilizar . Un desarrollador de JavaScript debe comprender bien esto.
Skillbox recomienda:
- Curso práctico .
- Curso en línea aplicado .
- Curso práctico de dos años .
Fuente: habr.com
