Async afventer JavaScript-vejledning - Sådan venter du på, at en funktion afsluttes i JS

Hvornår slutter en asynkron funktion? Og hvorfor er dette så svært spørgsmål at besvare?

Nå viser det sig, at forståelse af asynkrone funktioner kræver stor viden om, hvordan JavaScript fungerer grundlæggende.

Lad os udforske dette koncept og lære meget om JavaScript i processen.

Er du klar? Lad os gå.

Hvad er asynkron kode?

JavaScript er et synkront programmeringssprog efter design. Dette betyder, at når kode udføres, starter JavaScript øverst i filen og løber gennem kode linje for linje, indtil det er gjort.

Resultatet af denne designbeslutning er, at kun én ting kan ske ad gangen.

Du kan tænke på dette som om du jonglerede med seks små bolde. Mens du jonglerer, er dine hænder optaget og kan ikke håndtere noget andet.

Det er det samme med JavaScript: når koden kører, har den hænderne fulde med den kode. Vi kalder det denne form for synkron kode blokering . Fordi det effektivt blokerer for, at anden kode kører.

Lad os cirkel tilbage til jongleringseksemplet. Hvad ville der ske, hvis du ville tilføje endnu en bold? I stedet for seks bolde ville du jonglere med syv bolde. Det kan være et problem.

Du vil ikke stoppe jonglering, for det er bare så sjovt. Men du kan heller ikke gå og hente en ny bold, for det betyder at du bliver nødt til at stoppe.

Løsningen? Delegere arbejdet til en ven eller et familiemedlem. De jonglerer ikke, så de kan gå og hente bolden til dig og derefter smide den ind i din jonglering på et tidspunkt, hvor din hånd er fri, og du er klar til at tilføje endnu en bold midt i jonglering.

Dette er hvad asynkron kode er. JavaScript delegerer arbejdet til noget andet, hvorefter det handler om sin egen forretning. Så når den er klar, vil den modtage resultaterne tilbage fra arbejdet.

Hvem laver det andet arbejde?

Okay, så vi ved, at JavaScript er synkron og doven. Det ønsker ikke at udføre alt arbejdet selv, så det udbruger det til noget andet.

Men hvem er denne mystiske enhed, der arbejder for JavaScript? Og hvordan bliver det ansat til at arbejde for JavaScript?

Lad os se på et eksempel på asynkron kode.

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

Kørsel af denne kode resulterer i følgende output i konsollen:

// in console Hi there Han

I orden. Hvad sker der?

Det viser sig, at den måde, hvorpå vi arbejder ud i JavaScript, er at bruge miljøspecifikke funktioner og API'er. Og dette er en kilde til stor forvirring i JavaScript.

JavaScript kører altid i et miljø.

Ofte er dette miljø browseren. Men det kan også være på serveren med NodeJS. Men hvad i alverden er forskellen?

Forskellen - og dette er vigtigt - er, at browseren og serveren (NodeJS), funktionelt set, ikke er ækvivalente. De er ofte ens, men de er ikke de samme.

Lad os illustrere dette med et eksempel. Lad os sige, at JavaScript er hovedpersonen i en episk fantasibog. Bare en almindelig bondegutt.

Lad os sige, at dette bondegutt fandt to dragter af speciel rustning, der gav dem kræfter ud over deres egne.

Da de brugte browser rustning, fik de adgang til et bestemt sæt funktioner.

Da de brugte server rustning, fik de adgang til et andet sæt funktioner.

Disse dragter har en vis overlapning, fordi skaberne af disse dragter havde de samme behov visse steder, men ikke andre.

Dette er, hvad et miljø er. Et sted hvor koden køres, hvor der findes værktøjer, der er bygget oven på det eksisterende JavaScript-sprog. De er ikke en del af sproget, men linjen er ofte sløret, fordi vi bruger disse værktøjer hver dag, når vi skriver kode.

setTimeout, fetch og DOM er alle eksempler på web-API'er. (Du kan se den fulde liste over web-API'er her.) De er værktøjer, der er indbygget i browseren, og som gøres tilgængelige for os, når vores kode køres.

Og fordi vi altid kører JavaScript i et miljø, ser det ud til, at disse er en del af sproget. Men det er de ikke.

Så hvis du nogensinde har spekuleret på, hvorfor du kan bruge hentning i JavaScript, når du kører den i browseren (men har brug for at installere en pakke, når du kører den i NodeJS), er det derfor. Nogen troede, at hentning var en god idé og byggede den som et værktøj til NodeJS-miljøet.

Forvirrende? Ja!

Men nu kan vi endelig forstå, hvad der påtager sig arbejdet fra JavaScript, og hvordan det bliver ansat.

Det viser sig, at det er miljøet, der påtager sig arbejdet, og måden at få miljøet til at udføre det arbejde er at bruge funktionalitet, der hører til miljøet. For eksempel hente eller indstille Timeout i browsermiljøet.

Hvad sker der med arbejdet?

Store. Så miljøet påtager sig arbejdet. Hvad så?

På et eller andet tidspunkt er du nødt til at få resultaterne tilbage. Men lad os tænke på, hvordan dette ville fungere.

Lad os gå tilbage til jongleringseksemplet fra starten. Forestil dig, at du bad om en ny bold, og en ven begyndte lige at kaste bolden på dig, når du ikke var klar.

Det ville være en katastrofe. Måske kunne du være heldig og fange det og få det til din rutine effektivt. Men der er stor chance for, at det kan få dig til at tabe alle dine bolde og gå ned i din rutine. Ville det ikke være bedre, hvis du gav strenge instruktioner om, hvornår du skulle modtage bolden?

Som det viser sig, er der strenge regler omkring, hvornår JavaScript kan modtage delegeret arbejde.

Disse regler styres af begivenhedssløjfen og involverer mikrotask- og makrotask-køen. Ja, det ved jeg. Det er meget. Men bær med mig.

I orden. Så når vi delegerer asynkron kode til browseren, tager browseren og kører koden og påtager sig denne arbejdsbyrde. Men der kan være flere opgaver, der gives til browseren, så vi skal sørge for, at vi kan prioritere disse opgaver.

Det er her, mikrotaskekøen og makrotaskekøen kommer i spil. Browseren tager arbejdet, gør det og placerer derefter resultatet i en af ​​de to køer baseret på den type arbejde, den modtager.

Løfter placeres for eksempel i mikrotaskekøen og har højere prioritet.

Events og setTimeout er eksempler på arbejde, der placeres i makrotask-køen og har en lavere prioritet.

Når arbejdet er færdigt og placeret i en af ​​de to køer, kører begivenhedssløjfen frem og tilbage og kontrollerer, om JavaScript er klar til at modtage resultaterne eller ej.

Først når JavaScript er færdig med at køre al sin synkron kode og er god og klar, begynder begivenhedsløbet at vælge fra køerne og aflevere funktionerne tilbage til JavaScript for at køre.

Så lad os se på et eksempel:

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

Hvad vil ordren være her?

  1. For det første delegeres setTimeout til browseren, som udfører arbejdet og placerer den resulterende funktion i makrotask-køen.
  2. For det andet delegeres hentning til browseren, som tager arbejdet. Det henter dataene fra slutpunktet og placerer de resulterende funktioner i mikrotaskekøen.
  3. Javascript logger ud "Hvilken suppe"?
  4. Begivenhedssløjfen kontrollerer, om JavaScript er klar til at modtage resultaterne fra arbejdet i kø.
  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.

Der er også en serie i gang med at opbygge en hel app med React, hvis det er din marmelade. Og jeg planlægger at tilføje meget mere indhold her i fremtiden og gå i dybden med JavaScript-emner.

Og hvis du vil sige hej eller chatte om webudvikling, kan du altid kontakte mig på twitter på @foseberg. Tak for læsningen!