JavaScript se asinkronizira i čeka u petljama

Osnovno asynci awaitjednostavno. Stvari se malo zakompliciraju kada pokušate koristiti awaitpetlje.

U ovom članku želim podijeliti neke probleme na koje trebate pripaziti ako namjeravate koristiti awaitpetlje.

Prije nego što počneš

Pretpostavit ću da znate kako koristiti asynci await. Ako ne, pročitajte prethodni članak kako biste se upoznali prije nego što nastavite.

Priprema primjera

Za ovaj članak recimo da želite dobiti broj voća iz košarice s voćem.

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

Želite dobiti broj svakog voća iz fruitcoasketa. Da biste dobili broj ploda, možete koristiti getNumFruitfunkciju.

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

Recimo sada da fruitBasketživi na udaljenom poslužitelju. Pristup joj traje jednu sekundu. Ovom kašnjenju od jedne sekunde možemo se rugati s vremenskim ograničenjem. (Molimo pogledajte prethodni članak ako imate problema s razumijevanjem vremenskog ograničenja).

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

Na kraju, recimo da želite koristiti awaiti getNumFruitdobiti broj svakog voća u asinkronoj funkciji.

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

S ovim možemo početi gledati awaitu petlje.

Čekajte for for petlju

Recimo da imamo niz voća koje želimo dobiti iz košarice s voćem.

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

Krenut ćemo kroz ovaj niz.

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

U petlji for koristit ćemo getNumFruitza dobivanje broja svakog voća. Također ćemo prijaviti broj u konzolu.

Budući da getNumFruitvraća obećanje, možemo awaitriješiti vrijednost prije prijave.

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

Kada upotrebljavate await, očekujete da JavaScript zaustavi izvršenje dok se očekivano obećanje ne riješi. To znači da se awaits u for-loop treba izvoditi u seriji.

Rezultat je ono što biste očekivali.

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

Ovo ponašanje radi s većinom petlji (poput whilei for-ofpetlji) ...

Ali neće raditi s petljama koje zahtijevaju povratni poziv. Primjeri takvih petlji koje zahtijevaju vraćanje uključuju forEach, map, filter, i reduce. Mi ćemo pogledati kako awaitutječe forEach, mapi filteru sljedećih nekoliko dijelova.

Čekajte u forEach petlji

Učinit ćemo isto što i u primjeru for-loop. Prvo, krenimo kroz niz plodova.

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

Dalje, pokušat ćemo dobiti broj voća getNumFruit. (Primijetite asyncključnu riječ u funkciji povratnog poziva. Ova nam asyncključna riječ treba jer awaitje u funkciji povratnog poziva).

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

Mogli biste očekivati ​​da konzola izgleda ovako:

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

Ali stvarni rezultat je drugačiji. JavaScript nastavlja s pozivom console.log('End') prije nego što se riješe obećanja u forEach petlji.

Konzola se zapisuje ovim redoslijedom:

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

JavaScript to čini jer forEachnije svjestan obećanja. Ne može podržati asynci await. Ne možete _ koristiti awaitu forEach.

Čekajte s mapom

Ako koristite awaitu map, mapuvijek će vratiti niz obećanja. To je zato što asinkrone funkcije uvijek vraćaju obećanja.

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

Budući da mapuvijek vraćate obećanja (ako koristite await), morate pričekati da se niz obećanja riješi. To možete učiniti s 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”); };

Evo što dobivate:

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

Ako želite, možete manipulirati vrijednošću koju vraćate u svojim obećanjima. Razriješene vrijednosti bit će vrijednosti koje ste vratili.

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

Čekajte s filtrom

Kada upotrebljavate filter, želite filtrirati niz s određenim rezultatom. Recimo da želite stvoriti niz s više od 20 plodova.

Ako upotrebljavate filternormalno (bez čekanja), upotrijebit ćete ga ovako:

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

Očekivali biste moreThan20da sadrži samo jabuke jer ima 27 jabuka, ali ima 0 grožđa i 14 krušaka.

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

awaitu filterne radi na isti način. Zapravo uopće ne ide. Vratit ćete nefiltrirani niz ...

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.