ЈаваСцрипт се асинхронизује и чека у петљама

Основно asyncи awaitједноставно. Ствари постају мало компликованије када покушате да користите awaitпетље.

У овом чланку желим да поделим неке проблеме на које треба пазити ако намеравате да користите awaitпетље.

Пре него што почнете

Претпоставићу да знате како да користите asyncи await. Ако не, прочитајте претходни чланак да бисте се упознали пре него што наставите.

Припрема примера

За овај чланак рецимо да желите да добијете број воћа из корпе са воћем.

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

Желите да добијете број сваког воћа из фруитБаскет-а. Да бисте добили број плода, можете користити getNumFruitфункцију.

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

Рецимо сада да fruitBasketживи на удаљеном серверу. Приступ јој траје једну секунду. Овом кашњењу од једне секунде можемо да се ругамо с временским ограничењем. (Погледајте претходни чланак ако имате проблема са разумевањем кода за временско ограничење).

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

На крају, рецимо да желите да користите awaitи getNumFruitда бисте добили број сваког воћа у асинхроној функцији.

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

Са овим можемо почети да гледамо awaitу петље.

Чекајте фор фор петљу

Рецимо да имамо низ воћа које желимо да добијемо из корпе са воћем.

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

Проћи ћемо кроз овај низ.

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

У фор-лооп-у ћемо getNumFruitдобити број сваког воћа. Такође ћемо пријавити број у конзолу.

Пошто getNumFruitвраћа обећање, можемо awaitрешити вредност пре него што га пријавимо.

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

Када користите await, очекујете да ЈаваСцрипт заустави извршење док се очекивано обећање не реши. То значи да awaitс у фор-лооп треба да се извршава у серији.

Резултат је оно што бисте очекивали.

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

Ово понашање функционише са већином петљи (лике whileи for-ofпетље) ...

Али то неће радити са петљама које захтевају повратни позив. Примери таквих петљи које захтевају резервну укључују forEach, map, filterи reduce. Ми ћемо погледати како awaitутиче forEach, mapи filterу наредних неколико секција.

Чекајте форЕацх петљу

Урадићемо исто што и у примеру фор-лооп. Прво, кренимо кроз низ плодова.

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

Затим ћемо покушати да утврдимо број воћа getNumFruit. (Обратите пажњу на asyncкључну реч у функцији повратног позива. Ова asyncкључна реч нам је потребна јер awaitје у функцији повратног позива).

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

Можете очекивати да конзола изгледа овако:

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

Али стварни резултат је другачији. ЈаваСцрипт наставља да позива console.log('End') пре него што се обећања у форЕацх петљи реше.

Конзола се евидентира овим редоследом:

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

ЈаваСцрипт то ради зато што forEachније свестан обећања. Не може да подржи asyncи await. Не можете _ да користите awaitу forEach.

Чекајте са мапом

Ако користите awaitу map, mapувек ће вратити низ обећања. То је зато што асинхроне функције увек враћају обећања.

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

Будући да mapувек враћате обећања (ако користите await), морате сачекати да се низ обећања реши. Можете то учинити са 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”); };

Ево шта добијате:

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

Ако желите, можете манипулисати вредношћу коју враћате у обећањима. Разрешене вредности биће вредности које враћате.

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

Чекајте са филтером

Када користите filter, желите да филтрирате низ са одређеним резултатом. Рецимо да желите да направите низ са више од 20 плодова.

Ако користите filterнормално (без чекања), користићете га овако:

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

Очекивали moreThan20бисте да садржи само јабуке јер има 27 јабука, али има 0 грожђа и 14 крушака.

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

awaitу filterне ради на исти начин. У ствари, то уопште не функционише. Добијате нефилтрирани низ назад ...

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решене вредности

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

Ову верзију је једноставно прочитати и разумети, а за израчунавање укупног броја плодова потребна јој је једна секунда.

Кључне Такеаваис

1. Ако желите да awaitпозиве извршавате у серији, користите for-loop(или било коју петљу без повратног позива).

2. Никада не користите awaitса forEach. for-loopУместо тога користите (или било коју петљу без повратног позива).

3. Не awaitунутра filterи reduce. Увек awaitниз обећања са map, тада filterили у reduceскладу с тим.

Овај чланак је првобитно објављен на мом блогу .

Пријавите се за мој билтен ако желите још чланака који ће вам помоћи да постанете бољи програмер за фронтенд.