JavaScript asynchronisieren und in Schleifen warten

Grundlegend asyncund awaiteinfach. Die Dinge werden etwas komplizierter, wenn Sie versuchen, sie awaitin Schleifen zu verwenden.

In diesem Artikel möchte ich einige Fallstricke teilen, auf die Sie achten sollten, wenn Sie beabsichtigen, sie awaitin Schleifen zu verwenden.

Bevor Sie beginnen

Ich gehe davon aus, dass Sie wissen, wie man asyncund verwendet await. Wenn Sie dies nicht tun, lesen Sie den vorherigen Artikel, um sich vertraut zu machen, bevor Sie fortfahren.

Ein Beispiel vorbereiten

Angenommen, Sie möchten für diesen Artikel die Anzahl der Früchte aus einem Obstkorb abrufen.

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

Sie möchten die Nummer jeder Frucht aus dem Obstkorb erhalten. Um die Nummer einer Frucht zu erhalten, können Sie eine getNumFruitFunktion verwenden.

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

Nehmen wir an, wir fruitBasketleben auf einem Remote-Server. Der Zugriff dauert eine Sekunde. Wir können diese Verzögerung von einer Sekunde mit einer Zeitüberschreitung verspotten. (Bitte lesen Sie den vorherigen Artikel, wenn Sie Probleme beim Verstehen des Timeout-Codes haben.)

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

Schließlich lassen Sie uns sagen , Sie verwenden möchten , awaitund getNumFruitdie Anzahl der einzelnen Früchte in asynchronen Funktion zu erhalten.

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

Damit können wir beginnen, awaitin Schleifen zu betrachten.

Warten Sie in einer for-Schleife

Nehmen wir an, wir haben eine Reihe von Früchten, die wir aus dem Obstkorb holen möchten.

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

Wir werden dieses Array durchlaufen.

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

In der for-Schleife werden wir getNumFruitdie Anzahl jeder Frucht ermitteln. Wir werden die Nummer auch in die Konsole einloggen.

Da getNumFruitein Versprechen zurückgegeben wird, können wir awaitden aufgelösten Wert vor der Protokollierung festlegen.

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

Wenn Sie verwenden await, erwarten Sie, dass JavaScript die Ausführung unterbricht, bis das erwartete Versprechen gelöst ist. Dies bedeutet, dass awaits in einer for-Schleife in Reihe ausgeführt werden sollte.

Das Ergebnis ist das, was Sie erwarten würden.

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

Dieses Verhalten funktioniert mit den meisten Schleifen (wie whileund for-ofSchleifen) ...

Bei Schleifen, die einen Rückruf erfordern, funktioniert dies jedoch nicht. Beispiele für solche Schleifen , die einen Rückfall erfordern , umfassen forEach, map, filter, und reduce. Wir werden sehen, wie awaitwirkt forEach, mapund filterin den nächsten Abschnitten.

Warten Sie in einer forEach-Schleife

Wir machen dasselbe wie im for-loop-Beispiel. Lassen Sie uns zunächst die Reihe der Früchte durchgehen.

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

Als nächstes werden wir versuchen, die Anzahl der Früchte mit zu ermitteln getNumFruit. (Beachten Sie das asyncSchlüsselwort in der Rückruffunktion. Wir benötigen dieses asyncSchlüsselwort, da awaites in der Rückruffunktion enthalten ist.)

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

Sie können erwarten, dass die Konsole folgendermaßen aussieht:

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

Das tatsächliche Ergebnis ist jedoch anders. JavaScript ruft weiter auf, console.log('End') bevor die Versprechen in der forEach-Schleife aufgelöst werden.

Die Konsole meldet sich in dieser Reihenfolge an:

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

JavaScript tut dies, weil forEaches nicht vielversprechend ist. Es kann nicht unterstützen asyncund await. Sie _cannot_ verwenden awaitin forEach.

Warten Sie mit Karte

Wenn Sie awaitin a verwenden map, mapwird immer eine Reihe von Versprechen zurückgegeben. Dies liegt daran, dass asynchrone Funktionen immer Versprechen zurückgeben.

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”;

Da Sie mapimmer Versprechen zurückgeben (wenn Sie diese verwenden await), müssen Sie warten, bis die Reihe der Versprechen gelöst ist. Sie können dies mit tun 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”); };

Folgendes erhalten Sie:

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

Sie können den Wert, den Sie in Ihren Versprechen zurückgeben, manipulieren, wenn Sie möchten. Die aufgelösten Werte sind die Werte, die Sie zurückgeben.

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”;

Warten Sie mit Filter

Wenn Sie verwenden filter, möchten Sie ein Array mit einem bestimmten Ergebnis filtern. Angenommen, Sie möchten ein Array mit mehr als 20 Früchten erstellen.

Wenn Sie filternormal verwenden (ohne zu warten), verwenden Sie es folgendermaßen:

// 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’) }

Sie würden erwarten moreThan20, nur Äpfel zu enthalten, weil es 27 Äpfel gibt, aber es gibt 0 Trauben und 14 Birnen.

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

awaitin filterfunktioniert nicht auf die gleiche Weise. Tatsächlich funktioniert es überhaupt nicht. Sie erhalten das ungefilterte Array zurück ...

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.