JavaScript asinhronizējas un gaida ciklos

Pamata asyncun awaitir vienkārša. Lietas kļūst mazliet sarežģītākas, kad mēģināt izmantot awaitcilpas.

Šajā rakstā es vēlos dalīties ar dažiem gotchas, no kuriem jāuzmanās, ja jūs plānojat izmantot awaitcilpas.

Pirms tu sāc

Es pieņemšu, ka jūs zināt, kā lietot asyncun await. Ja to nedarāt, pirms turpināt, izlasiet iepriekšējo rakstu, lai iepazītos ar sevi.

Sagatavojot piemēru

Pieņemsim, ka par šo rakstu vēlaties iegūt augļu skaitu no augļu groza.

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

Jūs vēlaties iegūt katra augļa skaitu no fruitBasket. Lai iegūtu augļu skaitu, varat izmantot getNumFruitfunkciju.

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

Pieņemsim, ka fruitBasketdzīvo uz attālā servera. Piekļuve tam aizņem vienu sekundi. Mēs varam izsmiet šo vienas sekundes aizkavi ar noildzi. (Lūdzu, skatiet iepriekšējo rakstu, ja jums ir problēmas saprast noildzes kodu).

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

Visbeidzot, pieņemsim, ka jūs vēlaties izmantot awaitun getNumFruitiegūt asinhronās funkcijas katra augļa skaitu.

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

Ar to mēs varam sākt skatīties awaitcilpās.

Gaidiet cilni

Pieņemsim, ka mums ir augļu klāsts, ko mēs vēlamies iegūt no augļu groza.

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

Mēs gatavojamies cilpu caur šo masīvu.

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

For-loop mēs izmantosim, getNumFruitlai iegūtu katra augļa skaitu. Mēs arī reģistrēsim numuru konsolē.

Tā kā getNumFruitatgriež solījumu, mēs varam awaitatrisināt vērtību pirms tās reģistrēšanas.

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

Lietojot await, jūs sagaidāt, ka JavaScript apturēs izpildi, līdz gaidītais solījums tiks atrisināts. Tas nozīmē, ka awaits for-loop vajadzētu izpildīt virknē.

Rezultāts ir tāds, kādu jūs varētu sagaidīt.

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

Šī darbība darbojas ar lielāko daļu cilpu (piemēram, whileun for-ofcilpas) ...

Bet tas nedarbosies ar cilpām, kurām nepieciešama atzvanīšana. Piemēri cilpas, kas prasa atkāpšanos ietver forEach, map, filter, un reduce. Mēs skatāmies, kā awaitietekmē forEach, mapun filtertuvāko sadaļās.

Gaidiet forEach cilpu

Mēs darīsim to pašu, ko mēs darījām for-loop piemērā. Vispirms aplūkosim augļu masīvu.

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

Tālāk mēs centīsimies iegūt augļu skaitu ar getNumFruit. ( asyncAtzvana funkcijā ievērojiet atslēgvārdu. Šis asyncatslēgvārds mums ir nepieciešams, jo tas awaitir atzvanīšanas funkcijā).

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

Jūs varētu sagaidīt, ka konsole izskatīsies šādi:

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

Bet faktiskais rezultāts ir atšķirīgs. JavaScript turpina zvanīt, console.log('End') pirms solījumi forEach cilpā tiek atrisināti.

Konsole reģistrējas šādā secībā:

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

JavaScript to dara, jo forEachnav informēts par solījumiem. Tas nevar atbalstīt asyncun await. Jūs _cannot_ izmantot awaitin forEach.

Gaidi ar karti

Ja izmantojat awaita map, mapvienmēr atgriezīs solījumu masīvu. Tas ir tāpēc, ka asinhronās funkcijas vienmēr sniedz solījumus.

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

Tā kā mapvienmēr atgrieziet solījumus (ja izmantojat await), jums jāgaida, kamēr solījumu masīvs tiks atrisināts. To var izdarīt ar 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”); };

Lūk, ko jūs saņemat:

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

Ja vēlaties, varat manipulēt ar solījumos atgriezto vērtību. Atrisinātās vērtības būs tās vērtības, kuras jūs atgriezīsit.

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

Gaidiet ar filtru

Kad izmantojat filter, vēlaties filtrēt masīvu ar noteiktu rezultātu. Pieņemsim, ka vēlaties izveidot masīvu ar vairāk nekā 20 augļiem.

Ja izmantojat filterparasti (negaidot), izmantosiet to šādi:

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

Jūs varētu sagaidīt, moreThan20ka tajā būs tikai āboli, jo ir 27 āboli, bet ir 0 vīnogas un 14 bumbieri.

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

awaitin filternedarbojas tāpat. Patiesībā tas vispār nedarbojas. Jūs atgūsiet nefiltrēto masīvu ...

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. reduceatrisinātās vērtības

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

Šī versija ir viegli lasāma un saprotama, un, lai aprēķinātu kopējo augļu skaitu, ir nepieciešama viena sekunde.

Key Takeaways

1. Ja vēlaties veikt awaitzvanus sērijveidā, izmantojiet for-loop(vai jebkuru cilpu bez atzvanīšanas).

2. Nekad nelietojiet awaitar forEach. Tā vietā izmantojiet for-loop(vai jebkuru cilpu bez atzvanīšanas).

3. Nelieciet awaitiekšā filterun reduce. Vienmēr awaitar solījumiem map, tad filtervai reduceattiecīgi.

Šis raksts sākotnēji tika publicēts manā emuārā .

Reģistrējieties manam biļetenam, ja vēlaties vairāk rakstu, kas palīdzēs jums kļūt par labāku frontend izstrādātāju.