Async Await JavaScript Tutorial - Πώς να περιμένετε να ολοκληρωθεί μια λειτουργία στο JS

Πότε τελειώνει μια ασύγχρονη λειτουργία; Και γιατί είναι τόσο δύσκολο να απαντήσετε;

Λοιπόν, αποδεικνύεται ότι η κατανόηση ασύγχρονων λειτουργιών απαιτεί πολλές γνώσεις σχετικά με τον τρόπο λειτουργίας του JavaScript βασικά.

Ας πάμε να εξερευνήσουμε αυτήν την ιδέα και να μάθουμε πολλά για τη JavaScript στη διαδικασία.

Είσαι έτοιμος? Πάμε.

Τι είναι ο ασύγχρονος κώδικας;

Σχετικά με το σχεδιασμό, το JavaScript είναι μια σύγχρονη γλώσσα προγραμματισμού. Αυτό σημαίνει ότι όταν εκτελείται ο κώδικας, το JavaScript ξεκινά στο πάνω μέρος του αρχείου και περνά μέσα από τον κώδικα γραμμή προς γραμμή, έως ότου ολοκληρωθεί.

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

Μπορείτε να το σκεφτείτε σαν να κάνετε ζογκλέρ έξι μικρές μπάλες. Ενώ κάνετε ζογκλέρ, τα χέρια σας είναι απασχολημένα και δεν μπορούν να χειριστούν τίποτα άλλο.

Είναι το ίδιο με το JavaScript: όταν ο κώδικας εκτελείται, έχει τα χέρια του γεμάτα με αυτόν τον κωδικό. Το ονομάζουμε αυτό το είδος συγχρονισμού αποκλεισμού κώδικα . Επειδή εμποδίζει αποτελεσματικά την εκτέλεση άλλου κώδικα.

Ας επιστρέψουμε στο παράδειγμα της ζογκλέρ. Τι θα συνέβαινε αν θέλατε να προσθέσετε μια άλλη μπάλα; Αντί για έξι μπάλες, θέλατε να κάνετε ζογκλέρ με επτά μπάλες. Αυτό μπορεί να είναι ένα πρόβλημα.

Δεν θέλετε να σταματήσετε το ζογκλέρ, γιατί είναι πολύ διασκεδαστικό. Αλλά δεν μπορείτε να πάρετε και άλλη μπάλα, γιατί αυτό θα σήμαινε ότι θα πρέπει να σταματήσετε.

Η λύση? Αναθέστε την εργασία σε έναν φίλο ή μέλος της οικογένειας. Δεν κάνουν ταχυδακτυλουργίες, οπότε μπορούν να πάνε και να πάρουν την μπάλα για εσάς και, στη συνέχεια, να την πετάξουν στο ταχυδακτυλουργία σας σε μια στιγμή που το χέρι σας είναι ελεύθερο και είστε έτοιμοι να προσθέσετε μια άλλη μπάλα στη μέση.

Αυτός είναι ο ασύγχρονος κώδικας. Το JavaScript μεταβιβάζει το έργο σε κάτι άλλο και μετά αφορά τη δική του δουλειά. Στη συνέχεια, όταν είναι έτοιμο, θα λάβει τα αποτελέσματα πίσω από την εργασία.

Ποιος κάνει την άλλη δουλειά;

Εντάξει, έτσι γνωρίζουμε ότι η JavaScript είναι σύγχρονη και τεμπέλη. Δεν θέλει να κάνει όλη τη δουλειά από μόνη της, οπότε το καλλιεργεί σε κάτι άλλο.

Αλλά ποια είναι αυτή η μυστηριώδης οντότητα που λειτουργεί για JavaScript; Και πώς προσλαμβάνεται για να εργαστεί για JavaScript;

Λοιπόν, ας ρίξουμε μια ματιά σε ένα παράδειγμα ασύγχρονου κώδικα.

const logName = () => { console.log("Han") } setTimeout(logName, 0) console.log("Hi there")

Η εκτέλεση αυτού του κώδικα έχει ως αποτέλεσμα την ακόλουθη έξοδο στην κονσόλα:

// in console Hi there Han

Καλώς. Τι συμβαίνει?

Αποδεικνύεται ότι ο τρόπος με τον οποίο δουλεύουμε σε JavaScript είναι να χρησιμοποιούμε λειτουργίες και API ειδικά για το περιβάλλον. Και αυτή είναι μια πηγή μεγάλης σύγχυσης στο JavaScript.

Η JavaScript εκτελείται πάντα σε περιβάλλον.

Συχνά, αυτό το περιβάλλον είναι το πρόγραμμα περιήγησης. Αλλά μπορεί επίσης να βρίσκεται στο διακομιστή με το NodeJS. Αλλά τι είναι η διαφορά;

Η διαφορά - και αυτό είναι σημαντικό - είναι ότι το πρόγραμμα περιήγησης και ο διακομιστής (NodeJS), λειτουργικά, δεν είναι ισοδύναμα. Συχνά είναι παρόμοια, αλλά δεν είναι τα ίδια.

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

Τώρα ας πούμε ότι αυτό το παιδί αγρόκτημα βρήκε δύο στολές ειδικής πανοπλίας που τους έδιναν δυνάμεις πέρα ​​από τη δική τους.

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

Όταν χρησιμοποίησαν το πανοπλία του διακομιστή, απέκτησαν πρόσβαση σε ένα άλλο σύνολο δυνατοτήτων.

Αυτά τα κοστούμια έχουν κάποια αλληλεπικάλυψη, επειδή οι δημιουργοί αυτών των κοστουμιών είχαν τις ίδιες ανάγκες σε ορισμένα μέρη, αλλά όχι σε άλλα.

Αυτό είναι το περιβάλλον. Ένα μέρος όπου εκτελείται ο κώδικας, όπου υπάρχουν εργαλεία που είναι χτισμένα πάνω από την υπάρχουσα γλώσσα JavaScript. Δεν αποτελούν μέρος της γλώσσας, αλλά η γραμμή είναι συχνά θολή επειδή χρησιμοποιούμε αυτά τα εργαλεία κάθε μέρα όταν γράφουμε κώδικα.

Το setTimeout, το fetch και το DOM είναι όλα παραδείγματα API Ιστού. (Μπορείτε να δείτε την πλήρη λίστα των API Ιστού εδώ.) Πρόκειται για εργαλεία που είναι ενσωματωμένα στο πρόγραμμα περιήγησης και τα οποία διατίθενται σε εμάς όταν εκτελείται ο κώδικάς μας.

Και επειδή εκτελούμε πάντα JavaScript σε ένα περιβάλλον, φαίνεται ότι αυτά είναι μέρος της γλώσσας. Όμως δεν είναι.

Επομένως, αν έχετε αναρωτηθεί ποτέ γιατί μπορείτε να χρησιμοποιήσετε τη λήψη στο JavaScript όταν το εκτελείτε στο πρόγραμμα περιήγησης (αλλά πρέπει να εγκαταστήσετε ένα πακέτο όταν το εκτελείτε στο NodeJS), αυτός είναι ο λόγος. Κάποιος πίστευε ότι η ανάκτηση ήταν καλή ιδέα και την έφτιαξε ως εργαλείο για το περιβάλλον NodeJS.

Μπερδεμένος; Ναί!

Αλλά τώρα μπορούμε να καταλάβουμε επιτέλους τι παίρνει η δουλειά από το JavaScript και πώς προσλαμβάνεται.

Αποδεικνύεται ότι είναι το περιβάλλον που αναλαμβάνει την εργασία και ο τρόπος για να κάνει το περιβάλλον να κάνει αυτήν την εργασία, είναι να χρησιμοποιήσετε τη λειτουργικότητα που ανήκει στο περιβάλλον. Για παράδειγμα, λήψη ή setTimeout στο περιβάλλον του προγράμματος περιήγησης.

Τι συμβαίνει στην εργασία;

Μεγάλος. Έτσι το περιβάλλον αναλαμβάνει τη δουλειά. Και μετά τι?

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

Ας επιστρέψουμε στο παράδειγμα της ζογκλέρ από την αρχή. Φανταστείτε ότι ζητήσατε μια νέα μπάλα και ένας φίλος μόλις άρχισε να σας ρίχνει την μπάλα όταν δεν ήσασταν έτοιμοι.

Αυτό θα ήταν καταστροφή. Ίσως θα μπορούσατε να είστε τυχεροί και να το πιάσετε και να το κάνετε αποτελεσματικά στη ρουτίνα σας. Αλλά υπάρχει μεγάλη πιθανότητα να σας κάνει να ρίξετε όλες τις μπάλες σας και να συντρίψετε τη ρουτίνα σας. Δεν θα ήταν καλύτερα αν δώσατε αυστηρές οδηγίες για το πότε θα λάβετε τη μπάλα;

Όπως αποδεικνύεται, υπάρχουν αυστηροί κανόνες σχετικά με το πότε η JavaScript μπορεί να λάβει κατ 'εξουσιοδότηση εργασία.

Αυτοί οι κανόνες διέπονται από το βρόχο συμβάντων και περιλαμβάνουν την ουρά μικροσκοπίου και μακροπρόσκοπου. Ναι ξέρω. Είναι πολύ. Αλλά αντέξτε μαζί μου.

Καλώς. Έτσι, όταν αναθέτουμε ασύγχρονο κώδικα στο πρόγραμμα περιήγησης, το πρόγραμμα περιήγησης παίρνει και τρέχει τον κώδικα και αναλαμβάνει αυτόν τον φόρτο εργασίας. Ωστόσο, ενδέχεται να υπάρχουν πολλές εργασίες που δίνονται στο πρόγραμμα περιήγησης, οπότε πρέπει να διασφαλίσουμε ότι μπορούμε να δώσουμε προτεραιότητα σε αυτές τις εργασίες.

Αυτό είναι όπου η ουρά microtask και η ουρά macrotask παίζουν. Το πρόγραμμα περιήγησης θα πάρει την εργασία, θα το κάνει και θα τοποθετήσει το αποτέλεσμα σε μία από τις δύο ουρές με βάση τον τύπο εργασίας που λαμβάνει.

Οι υποσχέσεις, για παράδειγμα, τοποθετούνται στην ουρά microtask και έχουν υψηλότερη προτεραιότητα.

Τα συμβάντα και το setTimeout είναι παραδείγματα εργασιών που τοποθετούνται στην ουρά macrotask και έχουν χαμηλότερη προτεραιότητα.

Τώρα μόλις ολοκληρωθεί η εργασία και τοποθετηθεί σε μία από τις δύο ουρές, ο βρόχος συμβάντος θα τρέξει μπρος-πίσω και θα ελέγξει αν η JavaScript είναι έτοιμη να λάβει τα αποτελέσματα ή όχι.

Only when JavaScript is done running all its synchronous code, and is good and ready, will the event loop start picking from the queues and handing the functions back to JavaScript to run.

So let's take a look at an example:

setTimeout(() => console.log("hello"), 0) fetch("//someapi/data").then(response => response.json()) .then(data => console.log(data)) console.log("What soup?")

What will the order be here?

  1. Firstly, setTimeout is delegated to the browser, which does the work and puts the resulting function in the macrotask queue.
  2. Secondly fetch is delegated to the browser, which takes the work. It retrieves the data from the endpoint and puts the resulting functions in the microtask queue.
  3. Javascript logs out "What soup"?
  4. The event loop checks whether or not JavaScript is ready to receive the results from the queued work.
  5. When the console.log is done, JavaScript is ready. The event loop picks queued functions from the microtask queue, which has a higher priority, and gives them back to JavaScript to execute.
  6. After the microtask queue is empty, the setTimeout callback is taken out of the macrotask queue and given back to JavaScript to execute.
In console: // What soup? // the data from the api // hello

Promises

Now you should have a good deal of knowledge about how asynchronous code is handled by JavaScript and the browser environment. So let's talk about promises.

A promise is a JavaScript construct that represents a future unknown value. Conceptually, a promise is just JavaScript promising to return a value. It could be the result from an API call, or it could be an error object from a failed network request. You're guaranteed to get something.

const promise = new Promise((resolve, reject) => { // Make a network request if (response.status === 200) { resolve(response.body) } else { const error = { ... } reject(error) } }) promise.then(res => { console.log(res) }).catch(err => { console.log(err) })

A promise can have the following states:

  • fulfilled - action successfully completed
  • rejected - action failed
  • pending - neither action has been completed
  • settled - has been fulfilled or rejected

A promise receives a resolve and a reject function that can be called to trigger one of these states.

One of the big selling points of promises is that we can chain functions that we want to happen on success (resolve) or failure (reject):

  • To register a function to run on success we use .then
  • To register a function to run on failure we use .catch
// Fetch returns a promise fetch("//swapi.dev/api/people/1") .then((res) => console.log("This function is run when the request succeeds", res) .catch(err => console.log("This function is run when the request fails", err) // Chaining multiple functions fetch("//swapi.dev/api/people/1") .then((res) => doSomethingWithResult(res)) .then((finalResult) => console.log(finalResult)) .catch((err => doSomethingWithErr(err))

Perfect. Now let's take a closer look at what this looks like under the hood, using fetch as an example:

const fetch = (url, options) => { // simplified return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() // ... make request xhr.onload = () => { const options = { status: xhr.status, statusText: xhr.statusText ... } resolve(new Response(xhr.response, options)) } xhr.onerror = () => { reject(new TypeError("Request failed")) } } fetch("//swapi.dev/api/people/1") // Register handleResponse to run when promise resolves .then(handleResponse) .catch(handleError) // conceptually, the promise looks like this now: // { status: "pending", onsuccess: [handleResponse], onfailure: [handleError] } const handleResponse = (response) => { // handleResponse will automatically receive the response, ¨ // because the promise resolves with a value and automatically injects into the function console.log(response) } const handleError = (response) => { // handleError will automatically receive the error, ¨ // because the promise resolves with a value and automatically injects into the function console.log(response) } // the promise will either resolve or reject causing it to run all of the registered functions in the respective arrays // injecting the value. Let's inspect the happy path: // 1. XHR event listener fires // 2. If the request was successfull, the onload event listener triggers // 3. The onload fires the resolve(VALUE) function with given value // 4. Resolve triggers and schedules the functions registered with .then 

So we can use promises to do asynchronous work, and to be sure that we can handle any result from those promises. That is the value proposition. If you want to know more about promises you can read more about them here and here.

When we use promises, we chain our functions onto the promise to handle the different scenarios.

This works, but we still need to handle our logic inside callbacks (nested functions) once we get our results back. What if we could use promises but write synchronous looking code? It turns out we can.

Async/Await

Async/Await is a way of writing promises that allows us to write asynchronous code in a synchronous way. Let's have a look.

const getData = async () => { const response = await fetch("//jsonplaceholder.typicode.com/todos/1") const data = await response.json() console.log(data) } getData()

Nothing has changed under the hood here. We are still using promises to fetch data, but now it looks synchronous, and we no longer have .then and .catch blocks.

Async / Await is actually just syntactic sugar providing a way to create code that is easier to reason about, without changing the underlying dynamic.

Let's take a look at how it works.

Async/Await lets us use generators to pause the execution of a function. When we are using async / await we are not blocking because the function is yielding the control back over to the main program.

Then when the promise resolves we are using the generator to yield control back to the asynchronous function with the value from the resolved promise.

You can read more here for a great overview of generators and asynchronous code.

In effect, we can now write asynchronous code that looks like synchronous code. Which means that it is easier to reason about, and we can use synchronous tools for error handling such as try / catch:

const getData = async () => { try { const response = await fetch("//jsonplaceholder.typicode.com/todos/1") const data = await response.json() console.log(data) } catch (err) { console.log(err) } } getData()

Alright. So how do we use it? In order to use async / await we need to prepend the function with async. This does not make it an asynchronous function, it merely allows us to use await inside of it.

Failing to provide the async keyword will result in a syntax error when trying to use await inside a regular function.

const getData = async () => { console.log("We can use await in this function") }

Because of this, we can not use async / await on top level code. But async and await are still just syntactic sugar over promises. So we can handle top level cases with promise chaining:

async function getData() { let response = await fetch('//apiurl.com'); } // getData is a promise getData().then(res => console.log(res)).catch(err => console.log(err); 

This exposes another interesting fact about async / await. When defining a function as async, it will always return a promise.

Using async / await can seem like magic at first. But like any magic, it's just sufficiently advanced technology that has evolved over the years. Hopefully now you have a solid grasp of the fundamentals, and can use async / await with confidence.

Conclusion

If you made it here, congrats. You just added a key piece of knowledge about JavaScript and how it works with its environments to your toolbox.

This is definitely a confusing subject, and the lines are not always clear. But now you hopefully have a grasp on how JavaScript works with asynchronous code in the browser, and a stronger grasp over both promises and async / await.

If you enjoyed this article, you might also enjoy my youtube channel. I currently have a web fundamentals series going where I go through HTTP, building web servers from scratch and more.

Υπάρχει επίσης μια σειρά που δημιουργεί μια ολόκληρη εφαρμογή με το React, αν αυτή είναι η εμπλοκή σας. Και σκοπεύω να προσθέσω πολύ περισσότερο περιεχόμενο εδώ στο μέλλον με βάθος σε θέματα JavaScript.

Και αν θέλετε να πείτε γεια ή να συνομιλήσετε για την ανάπτυξη ιστού, μπορείτε πάντα να επικοινωνήσετε μαζί μου στο twitter στο @foseberg. Ευχαριστώ για την ανάγνωση!