JavaScript async y espera en bucles

Básico asyncy awaitsencillo. Las cosas se complican un poco más cuando intentas usarlo awaiten bucles.

En este artículo, quiero compartir algunos trucos a tener en cuenta si tiene la intención de usar awaiten bucles.

Antes de que empieces

Voy a asumir que sabes cómo usar asyncy await. Si no es así, lea el artículo anterior para familiarizarse antes de continuar.

Preparando un ejemplo

Para este artículo, digamos que desea obtener la cantidad de frutas de una canasta de frutas.

const fruitBasket = { apple: 27, grape: 0, pear: 14 };

Desea obtener el número de cada fruta del fruitBasket. Para obtener el número de una fruta, puede usar una getNumFruitfunción.

const getNumFruit = fruit => { return fruitBasket[fruit]; }; const numApples = getNumFruit(“apple”); console.log(numApples); // 27

Ahora, digamos que fruitBasketvive en un servidor remoto. Acceder a él lleva un segundo. Podemos burlarnos de este retraso de un segundo con un tiempo de espera. (Consulte el artículo anterior si tiene problemas para comprender el código de tiempo de espera).

const sleep = ms => { return new Promise(resolve => setTimeout(resolve, ms)); }; const getNumFruit = fruit => { return sleep(1000).then(v => fruitBasket[fruit]); }; getNumFruit(“apple”).then(num => console.log(num)); // 27

Finalmente, digamos que desea usar awaity getNumFruitobtener el número de cada fruta en función asincrónica.

const control = async _ => { console.log(“Start”); const numApples = await getNumFruit(“apple”); console.log(numApples); const numGrapes = await getNumFruit(“grape”); console.log(numGrapes); const numPears = await getNumFruit(“pear”); console.log(numPears); console.log(“End”); };

Con esto, podemos comenzar a mirar awaiten bucles.

Espera en un bucle for

Digamos que tenemos una variedad de frutas que queremos obtener de la canasta de frutas.

const fruitsToGet = [“apple”, “grape”, “pear”];

Vamos a recorrer esta matriz.

const forLoop = async _ => { console.log(“Start”); for (let index = 0; index < fruitsToGet.length; index++) { // Get num of each fruit } console.log(“End”); };

En el bucle for, usaremos getNumFruitpara obtener el número de cada fruta. También registraremos el número en la consola.

Dado que getNumFruitdevuelve una promesa, podemos awaitcalcular el valor resuelto antes de registrarlo.

const forLoop = async _ => { console.log(“Start”); for (let index = 0; index < fruitsToGet.length; index++) { const fruit = fruitsToGet[index]; const numFruit = await getNumFruit(fruit); console.log(numFruit); } console.log(“End”); };

Cuando lo usa await, espera que JavaScript pause la ejecución hasta que se resuelva la promesa esperada. Esto significa que los awaits en un bucle for deberían ejecutarse en serie.

El resultado es el esperado.

“Start”; “Apple: 27”; “Grape: 0”; “Pear: 14”; “End”;

Este comportamiento funciona con la mayoría de los bucles (me gusta whiley for-ofbucles) ...

Pero no funcionará con bucles que requieran una devolución de llamada. Ejemplos de tales bucles que requieren un fallback incluyen forEach, map, filter, y reduce. Vamos a ver cómo awaitafecta forEach, mapy filteren las siguientes secciones.

Espera en un bucle forEach

Haremos lo mismo que hicimos en el ejemplo de bucle for. Primero, recorramos la variedad de frutas.

const forEachLoop = _ => { console.log(“Start”); fruitsToGet.forEach(fruit => { // Send a promise for each fruit }); console.log(“End”); };

A continuación, intentaremos obtener la cantidad de frutas con getNumFruit. (Observe la asyncpalabra clave en la función de devolución de llamada. Necesitamos esta asyncpalabra clave porque awaitestá en la función de devolución de llamada).

const forEachLoop = _ => { console.log(“Start”); fruitsToGet.forEach(async fruit => { const numFruit = await getNumFruit(fruit); console.log(numFruit); }); console.log(“End”); };

Es de esperar que la consola se vea así:

“Start”; “27”; “0”; “14”; “End”;

Pero el resultado real es diferente. JavaScript procede a llamar console.log('End') antes de que se resuelvan las promesas en el bucle forEach.

La consola inicia sesión en este orden:

‘Start’ ‘End’ ‘27’ ‘0’ ‘14’

JavaScript hace esto porque forEachno es compatible con las promesas. No puede soportar asyncy await. Usted _no puede_ utiliza awaiten forEach.

Espera con mapa

Si usa awaiten a map, mapsiempre devolverá una matriz de promesa. Esto se debe a que las funciones asincrónicas siempre devuelven promesas.

const mapLoop = async _ => { console.log(“Start”); const numFruits = await fruitsToGet.map(async fruit => { const numFruit = await getNumFruit(fruit); return numFruit; }); console.log(numFruits); console.log(“End”); }; “Start”; “[Promise, Promise, Promise]”; “End”;

Dado que mapsiempre devuelve las promesas (si las usa await), debe esperar a que se resuelva la matriz de promesas. Puedes hacer esto con await Promise.all(arrayOfPromises).

const mapLoop = async _ => { console.log(“Start”); const promises = fruitsToGet.map(async fruit => { const numFruit = await getNumFruit(fruit); return numFruit; }); const numFruits = await Promise.all(promises); console.log(numFruits); console.log(“End”); };

Esto es lo que obtienes:

“Start”; “[27, 0, 14]”; “End”;

Puede manipular el valor que devuelve en sus promesas si lo desea. Los valores resueltos serán los valores que devuelva.

const mapLoop = async _ => { // … const promises = fruitsToGet.map(async fruit => { const numFruit = await getNumFruit(fruit); // Adds onn fruits before returning return numFruit + 100; }); // … }; “Start”; “[127, 100, 114]”; “End”;

Espera con filtro

Cuando lo usa filter, desea filtrar una matriz con un resultado específico. Supongamos que desea crear una matriz con más de 20 frutas.

Si lo usa filternormalmente (sin esperar), lo usará así:

// Filter if there’s no await const filterLoop = _ => { console.log(‘Start’) const moreThan20 = await fruitsToGet.filter(fruit => { const numFruit = fruitBasket[fruit] return numFruit > 20 }) console.log(moreThan20) console.log(‘End’) }

Esperaría moreThan20contener solo manzanas porque hay 27 manzanas, pero hay 0 uvas y 14 peras.

“Start”[“apple”]; (“End”);

awaiten filterno funciona de la misma manera. De hecho, no funciona en absoluto. Obtienes la matriz sin filtrar ...

const filterLoop = _ => { console.log(‘Start’) const moreThan20 = await fruitsToGet.filter(async fruit => { const numFruit = getNumFruit(fruit) return numFruit > 20 }) console.log(moreThan20) console.log(‘End’) } “Start”[(“apple”, “grape”, “pear”)]; (“End”);

Here's why it happens.

When you use await in a filter callback, the callback always a promise. Since promises are always truthy, everything item in the array passes the filter. Writing await in a filter is like writing this code:

// Everything passes the filter… const filtered = array.filter(true);

There are three steps to use await and filter properly:

1. Use map to return an array promises

2. await the array of promises

3. filter the resolved values

const filterLoop = async _ => { console.log(“Start”); const promises = await fruitsToGet.map(fruit => getNumFruit(fruit)); const numFruits = await Promise.all(promises); const moreThan20 = fruitsToGet.filter((fruit, index) => { const numFruit = numFruits[index]; return numFruit > 20; }); console.log(moreThan20); console.log(“End”); }; Start[“apple”]; End;

Await with reduce

For this case, let's say you want to find out the total number of fruits in the fruitBastet. Normally, you can use reduce to loop through an array and sum the number up.

// Reduce if there’s no await const reduceLoop = _ => { console.log(“Start”); const sum = fruitsToGet.reduce((sum, fruit) => { const numFruit = fruitBasket[fruit]; return sum + numFruit; }, 0); console.log(sum); console.log(“End”); };

You'll get a total of 41 fruits. (27 + 0 + 14 = 41).

“Start”; “41”; “End”;

When you use await with reduce, the results get extremely messy.

// Reduce if we await getNumFruit const reduceLoop = async _ => { console.log(“Start”); const sum = await fruitsToGet.reduce(async (sum, fruit) => { const numFruit = await getNumFruit(fruit); return sum + numFruit; }, 0); console.log(sum); console.log(“End”); }; “Start”; “[object Promise]14”; “End”;

What?! [object Promise]14?!

Dissecting this is interesting.

  • In the first iteration, sum is 0. numFruit is 27 (the resolved value from getNumFruit(‘apple’)). 0 + 27 is 27.
  • In the second iteration, sum is a promise. (Why? Because asynchronous functions always return promises!) numFruit is 0. A promise cannot be added to an object normally, so the JavaScript converts it to [object Promise] string. [object Promise] + 0 is [object Promise]0
  • In the third iteration, sum is also a promise. numFruit is 14. [object Promise] + 14 is [object Promise]14.

Mystery solved!

This means, you can use await in a reduce callback, but you have to remember to await the accumulator first!

const reduceLoop = async _ => { console.log(“Start”); const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => { const sum = await promisedSum; const numFruit = await getNumFruit(fruit); return sum + numFruit; }, 0); console.log(sum); console.log(“End”); }; “Start”; “41”; “End”;

But... as you can see from the gif, it takes pretty long to await everything. This happens because reduceLoop needs to wait for the promisedSum to be completed for each iteration.

There's a way to speed up the reduce loop. (I found out about this thanks to Tim Oxley. If you await getNumFruits() first before await promisedSum, the reduceLoop takes only one second to complete:

const reduceLoop = async _ => { console.log(“Start”); const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => { // Heavy-lifting comes first. // This triggers all three getNumFruit promises before waiting for the next iteration of the loop. const numFruit = await getNumFruit(fruit); const sum = await promisedSum; return sum + numFruit; }, 0); console.log(sum); console.log(“End”); };

This works because reduce can fire all three getNumFruit promises before waiting for the next iteration of the loop. However, this method is slightly confusing since you have to be careful of the order you await things.

The simplest (and most efficient way) to use await in reduce is to:

1. Use map to return an array promises

2. await the array of promises

3. reduce the resolved values

const reduceLoop = async _ => { console.log(“Start”); const promises = fruitsToGet.map(getNumFruit); const numFruits = await Promise.all(promises); const sum = numFruits.reduce((sum, fruit) => sum + fruit); console.log(sum); console.log(“End”); };

This version is simple to read and understand, and takes one second to calculate the total number of fruits.

Key Takeaways

1. If you want to execute await calls in series, use a for-loop (or any loop without a callback).

2. Don't ever use await with forEach. Use a for-loop (or any loop without a callback) instead.

3. Don't await inside filter and reduce. Always await an array of promises with map, then filter or reduce accordingly.

This article was originally posted on my blog.

Sign up for my newsletter if you want more articles to help you become a better frontend developer.