Async čeka JavaScript JavaScript - Kako pričekati da funkcija završi u JS-u

Kada završava asinkrona funkcija? I zašto je na ovo tako teško pitanje odgovoriti?

Pa ispada da razumijevanje asinkronih funkcija zahtijeva veliko znanje o tome kako JavaScript temeljno djeluje.

Idemo istražiti ovaj koncept i naučiti puno o JavaScriptu u procesu.

Jesi li spreman? Idemo.

Što je asinkroni kod?

Po dizajnu, JavaScript je sinkroni programski jezik. To znači da kada se kôd izvrši, JavaScript započinje na vrhu datoteke i prolazi kroz kôd redak po redak, sve dok to ne učini.

Rezultat ove dizajnerske odluke je da se u isto vrijeme može dogoditi samo jedna stvar.

Možete to pomisliti kao da žonglirate sa šest malih kuglica. Dok žonglirate, ruke su vam zauzete i ne mogu podnijeti ništa drugo.

Isto je i s JavaScriptom: jednom kad se kôd pokrene, ima pune ruke posla s tim kodom. To nazivamo ovom vrstom sinkronog blokiranja koda . Jer učinkovito blokira pokretanje drugog koda.

Vratimo se na primjer žongliranja. Što bi se dogodilo da želite dodati još jednu loptu? Umjesto šest kuglica, htjeli ste žonglirati sa sedam kuglica. To bi mogao biti problem.

Ne želite prestati žonglirati, jer je to baš zabavno. Ali ni vi ne možete ići po drugu loptu, jer bi to značilo da biste morali stati.

Rješenje? Prenesite posao prijatelju ili članu obitelji. Oni ne žongliraju, pa mogu otići i uzeti loptu za vas, a zatim je baciti u svoje žongliranje u trenutku kada vam je ruka slobodna i spremni ste dodati još jednu loptu u sredini žongliranja.

To je ono što je asinkroni kod. JavaScript delegira rad na nešto drugo, a zatim se bavi vlastitim poslom. Tada, kad bude spremno, primit će rezultate natrag s rada.

Tko radi drugi posao?

U redu, dakle, znamo da je JavaScript sinkron i lijen. Ne želi sam obaviti cijeli posao, pa ga uzgaja na nešto drugo.

Ali tko je taj misteriozni entitet koji radi za JavaScript? I kako se zapošljava da radi za JavaScript?

Pa, pogledajmo primjer asinkronog koda.

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

Pokretanje ovog koda rezultira sljedećim izlazom u konzoli:

// in console Hi there Han

U redu. Što se događa?

Ispostavilo se da način na koji obrađujemo rad u JavaScript-u jest korištenje funkcija i API-ja specifičnih za okruženje. A ovo je izvor velike zabune u JavaScript-u.

JavaScript se uvijek pokreće u okruženju.

Često je to okruženje preglednik. Ali može biti i na poslužitelju s NodeJS-om. Ali u čemu je, pobogu, razlika?

Razlika - i ovo je važno - jest da preglednik i poslužitelj (NodeJS), funkcionalno gledano, nisu ekvivalentni. Često su slični, ali nisu isti.

Pokažimo to na primjeru. Recimo da je JavaScript glavni junak epske fantastične knjige. Samo obično dijete na farmi.

Sad recimo da je ovo dijete s farme pronašlo dva odijela posebnog oklopa koja su im davala moć veću od njihove.

Kada su koristili oklop za preglednik, stekli su pristup određenom skupu mogućnosti.

Kad su koristili poslužiteljsko odijelo, dobili su pristup drugom skupu mogućnosti.

Ta se odijela ponešto preklapaju, jer su tvorci tih odijela na određenim mjestima imali iste potrebe, ali na drugima nisu.

To je ono što je okruženje. Mjesto na kojem se pokreće kôd, gdje postoje alati koji su izgrađeni na vrhu postojećeg jezika JavaScript. Oni nisu dio jezika, ali linija je često zamagljena jer ove alate koristimo svaki dan kad pišemo kod.

setTimeout, dohvaćanje i DOM primjeri su web API-ja. (Ovdje možete vidjeti cijeli popis web API-ja.) To su alati koji su ugrađeni u preglednik i koji su nam dostupni prilikom pokretanja našeg koda.

A budući da JavaScript uvijek pokrećemo u okruženju, čini se da su oni dio jezika. Ali nisu.

Dakle, ako ste se ikad pitali zašto možete koristiti dohvaćanje u JavaScriptu kada ga pokrenete u pregledniku (ali morate instalirati paket kada ga pokrenete u NodeJS-u), to je razlog zašto. Netko je smatrao da je dohvat dobra ideja i izgradio ga je kao alat za okruženje NodeJS.

Zbunjujuće? Da!

Ali sada napokon možemo razumjeti što na poslu preuzima JavaScript i kako se zapošljava.

Ispostavilo se da je okolina ta koja preuzima posao, a način na koji se okolina mora baviti tim radom je korištenje funkcionalnosti koja pripada okolišu. Na primjer dohvatiti ili setTimeout u pregledniku okruženju.

Što se događa s djelom?

Sjajno. Dakle, okolina preuzima posao. Što onda?

U nekom trenutku trebate vratiti rezultate. Ali razmislimo kako bi ovo moglo funkcionirati.

Vratimo se primjeru žongliranja s početka. Zamislite da ste tražili novu loptu, a prijatelj vam je počeo bacati loptu kad niste bili spremni.

To bi bila katastrofa. Možda biste mogli imati sreće, uhvatiti je i učinkovito unijeti u svoju rutinu. No, postoji velika šansa da vam svibanj ispustite sve muda i srušite svoju rutinu. Ne bi li bilo bolje kad biste dali stroge upute o vremenu primanja lopte?

Ispostavilo se da postoje stroga pravila oko kojih JavaScript može primati delegirani rad.

Ta se pravila uređuju petljom događaja i uključuju red mikrozadatki i makrozadataka. Da znam. To je puno. Ali trpi sa mnom.

U redu. Dakle, kada dodijelimo asinkroni kôd pregledniku, preglednik uzima i pokreće kôd i preuzima to radno opterećenje. No, pregledniku se može dodijeliti više zadataka, pa moramo biti sigurni da im možemo zadati prioritet.

Tu ulaze u red red za mikrozadatke i red za makrozadatke. Preglednik će preuzeti posao, odraditi ga, a zatim rezultat smjestiti u jedan od dva reda na temelju vrste posla koji prima.

Na primjer, obećanja su stavljena u red mikrozadataka i imaju veći prioritet.

Događaji i setTimeout primjeri su posla koji se stavlja u red makrozadatki i ima niži prioritet.

Kad se posao završi i stavi u jedan od dva reda, petlja događaja pokrenut će se naprijed-natrag i provjeriti je li JavaScript spreman za primanje rezultata.

Tek kada JavaScript završi sa pokretanjem svog svog sinkronog koda i bude dobar i spreman, petlja događaja započet će odabirati iz redova i vraćati funkcije natrag u JavaScript za pokretanje.

Pogledajmo primjer:

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

Kakav će biti red ovdje?

  1. Prvo, setTimeout delegira se pregledniku koji obavlja posao i rezultirajuću funkciju stavlja u red makrozadatki.
  2. Kao drugo, preuzimanje je dodijeljeno pregledniku, koji preuzima posao. Dohvaća podatke s krajnje točke i rezultirajuće funkcije stavlja u red mikrozadataka.
  3. Javascript se odjavljuje s "Kakva juha"?
  4. Petlja događaja provjerava je li JavaScript spreman za primanje rezultata rada u redu.
  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.

There's also a series going on building an entire app with React, if that is your jam. And I plan to add much more content here in the future going in depth on JavaScript topics.

And if you want to say hi or chat about web development, you could always reach out to me on twitter at @foseberg. Thanks for reading!