JavaScript asynkroniseres og afventer i sløjfer

Grundlæggende asyncog awaiter enkel. Ting bliver lidt mere komplicerede, når du prøver at bruge awaiti sløjfer.

I denne artikel vil jeg dele nogle gotchas at passe på, hvis du har til hensigt at bruge awaiti sløjfer.

Før du begynder

Jeg antager, at du ved, hvordan du bruger asyncog await. Hvis du ikke gør det, skal du læse den forrige artikel for at gøre dig bekendt, inden du fortsætter.

Forbereder et eksempel

Lad os sige, at du til denne artikel vil hente antallet af frugter fra en frugtkurv.

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

Du ønsker at få antallet af hver frugt fra fruitBasket. For at få antallet af en frugt kan du bruge en getNumFruitfunktion.

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

Lad os sige, at vi fruitBasketbor på en ekstern server. Adgang til det tager et sekund. Vi kan spotte denne forsinkelse på et sekund med en timeout. (Se den forrige artikel, hvis du har problemer med at forstå timeout-koden).

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

Lad os endelig sige, at du vil bruge awaitog getNumFruitfå nummeret på hver frugt i asynkron funktion.

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

Med dette kan vi begynde at se på awaiti løkker.

Vent i en for løkke

Lad os sige, at vi har en række frugter, vi ønsker at få fra frugtkurven.

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

Vi vil løbe gennem denne matrix.

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

I for-loop bruger vi til getNumFruitat få antallet af hver frugt. Vi logger også nummeret ind i konsollen.

Da getNumFruitreturnerer et løfte, kan vi awaitden løste værdi, før vi logger den.

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

Når du bruger await, forventer du, at JavaScript stopper udførelsen, indtil det afventede løfte bliver løst. Dette betyder, at awaits i en for-loop skal udføres i serie.

Resultatet er, hvad du ville forvente.

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

Denne adfærd fungerer med de fleste sløjfer (ligesom whileog for-ofsløjfer) ...

Men det fungerer ikke med sløjfer, der kræver tilbagekald. Eksempler på sådanne sløjfer, der kræver en fallback indbefatter forEach, map, filter, og reduce. Vi vil se på, hvordan awaitpåvirker forEach, mapog filteri de næste par afsnit.

Vent i en forEach-løkke

Vi gør det samme som vi gjorde i for-loop-eksemplet. Lad os først løbe gennem frugtsamlingen.

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

Dernæst prøver vi at få antallet af frugter med getNumFruit. (Bemærk asyncnøgleordet i tilbagekaldsfunktionen. Vi har brug for dette asyncnøgleord, fordi det awaiter i tilbagekaldsfunktionen).

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

Du kan forvente, at konsollen ser sådan ud:

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

Men det faktiske resultat er anderledes. JavaScript fortsætter med at ringe, console.log('End') før løfterne i forEach-løkken bliver løst.

Konsollen logger i denne rækkefølge:

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

JavaScript gør dette, fordi forEachdet ikke er løftebevidst. Det kan ikke støtte asyncog await. Du _kan ikke_ bruge awaiti forEach.

Vent med kort

Hvis du bruger awaiti en map, mapvil altid returnere en række løfter. Dette skyldes, at asynkrone funktioner altid returnerer løfter.

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 du mapaltid returnerer løfter (hvis du bruger await), skal du vente på, at den række løfter, der bliver løst. Du kan gøre dette med 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”); };

Her er hvad du får:

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

Du kan manipulere den værdi, du returnerer i dine løfter, hvis du ønsker det. De løste værdier er de værdier, du returnerer.

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

Vent med filter

Når du bruger filter, vil du filtrere en matrix med et specifikt resultat. Lad os sige, at du vil oprette en matrix med mere end 20 frugter.

Hvis du bruger filternormalt (uden afventning), bruger du det således:

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

Man forventer kun moreThan20at indeholde æbler, fordi der er 27 æbler, men der er 0 druer og 14 pærer.

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

awaitin filterfungerer ikke på samme måde. Faktisk virker det slet ikke. Du får det ufiltrerede array tilbage ...

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. reducede opløste værdier

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

Denne version er enkel at læse og forstå og tager et sekund at beregne det samlede antal frugter.

Vigtigste takeaways

1. Hvis du vil udføre awaitopkald i serie, skal du bruge en for-loop(eller en hvilken som helst loop uden tilbagekald).

2. Brug aldrig nogensinde awaitmed forEach. Brug i stedet en for-loop(eller en hvilken som helst loop uden tilbagekald).

3. Må ikke awaitinde filterog reduce. Altid awaiten række løfter med map, derefter filtereller i reduceoverensstemmelse hermed.

Denne artikel blev oprindeligt sendt på min blog .

Tilmeld dig mit nyhedsbrev, hvis du vil have flere artikler, der hjælper dig med at blive en bedre frontend-udvikler.