Async JavaScript και περιμένετε σε βρόχους

Βασικό asyncκαι awaitαπλό. Τα πράγματα γίνονται λίγο πιο περίπλοκα όταν προσπαθείτε να χρησιμοποιήσετε awaitσε βρόχους.

Σε αυτό το άρθρο, θέλω να μοιραστώ μερικά gotchas για να προσέξετε εάν σκοπεύετε να χρησιμοποιήσετε 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”); };

Στο for-loop, θα χρησιμοποιήσουμε 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, αναμένεται να σταματήσει η εκτέλεση της εκτέλεσης έως ότου επιλυθεί η αναμενόμενη υπόσχεση. Αυτό σημαίνει ότι το awaits σε for-loop πρέπει να εκτελεστεί σε σειρά.

Το αποτέλεσμα είναι αυτό που θα περίμενε κανείς.

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

Αυτή η συμπεριφορά λειτουργεί με τους περισσότερους βρόχους (όπως whileκαι for-ofβρόχους) ...

Αλλά δεν θα λειτουργεί με βρόχους που απαιτούν επανάκληση. Παραδείγματα τέτοιων βρόχων που απαιτούν μια εναλλακτική περιλαμβάνουν forEach, map, filter, και reduce. Θα δούμε πώς awaitεπηρεάζει forEach, mapκαι filterμέσα στις επόμενες ενότητες.

Περιμένετε σε ένα βρόχο forEach

Θα κάνουμε το ίδιο πράγμα με το παράδειγμα για το βρόχο. Αρχικά, ας βρούμε τη σειρά των φρούτων.

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

Αλλά το πραγματικό αποτέλεσμα είναι διαφορετικό. Το JavaScript προχωρεί στην κλήση console.log('End') προτού επιλυθούν οι υποσχέσεις στο βρόχο forEach.

Η κονσόλα συνδέεται με αυτήν τη σειρά:

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

Το JavaScript το κάνει αυτό επειδή forEachδεν τηρεί τις υποσχέσεις. Δεν μπορεί να υποστηρίξει asyncκαι await. _ Δεν μπορείτε να χρησιμοποιήσετε awaitτο forEach.

Περιμένετε με χάρτη

Εάν χρησιμοποιείτε awaitτο a 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”);

Εδώ συμβαίνει.

Όταν χρησιμοποιείτε awaitμια filterεπανάκληση, η επιστροφή κλήσης είναι πάντα υπόσχεση. Δεδομένου ότι οι υποσχέσεις είναι πάντα αληθείς, όλα τα στοιχεία του πίνακα περνούν το φίλτρο. Το γράψιμο awaitσε ένα filterείναι σαν να γράφετε αυτόν τον κωδικό:

// Everything passes the filter… const filtered = array.filter(true);

Υπάρχουν τρία βήματα για χρήση awaitκαι filterσωστά:

1. Χρησιμοποιήστε το mapγια να επιστρέψετε υποσχέσεις πίνακα

2. awaitη σειρά των υποσχέσεων

3. filterοι επιλυμένες τιμές

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;

Περιμένετε με μείωση

Για αυτήν την περίπτωση, ας υποθέσουμε ότι θέλετε να μάθετε τον συνολικό αριθμό των φρούτων στο fruitBastet. Κανονικά, μπορείτε να χρησιμοποιήσετε reduceγια να περάσετε από έναν πίνακα και να αθροίσετε τον αριθμό.

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

Θα λάβετε συνολικά 41 φρούτα. (27 + 0 + 14 = 41).

“Start”; “41”; “End”;

Όταν χρησιμοποιείτε awaitμε μείωση, τα αποτελέσματα γίνονται εξαιρετικά ακατάστατα.

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

Τι?! [object Promise]14;

Η διάσπαση είναι ενδιαφέρουσα.

  • Στην πρώτη επανάληψη, sumείναι 0. numFruitείναι 27 (η τιμή που έχει επιλυθεί από getNumFruit(‘apple’)). 0 + 27 είναι 27.
  • Στη δεύτερη επανάληψη, sumείναι μια υπόσχεση. (Γιατί; Επειδή οι ασύγχρονες συναρτήσεις επιστρέφουν πάντα υποσχέσεις!) numFruitΕίναι 0. Μια υπόσχεση δεν μπορεί να προστεθεί σε ένα αντικείμενο κανονικά, επομένως η JavaScript τη μετατρέπει σε [object Promise]συμβολοσειρά. [object Promise] + 0 είναι[object Promise]0
  • Στην τρίτη επανάληψη, sumείναι επίσης μια υπόσχεση. numFruitείναι 14. [object Promise] + 14είναι [object Promise]14.

Το μυστήριο λύθηκε!

Αυτό σημαίνει ότι μπορείτε να χρησιμοποιήσετε awaitσε μια reduceεπιστροφή κλήσης, αλλά πρέπει να θυμάστε awaitπρώτα στον συσσωρευτή!

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ανάλογα.

Αυτό το άρθρο δημοσιεύτηκε αρχικά στο ιστολόγιό μου .

Εγγραφείτε στο ενημερωτικό δελτίο μου εάν θέλετε περισσότερα άρθρα που θα σας βοηθήσουν να γίνετε καλύτερος προγραμματιστής.