Async Oota JavaScripti õpetust - kuidas oodata, kuni funktsioon JS-is lõpeb

Millal lõpeb asünkroonne funktsioon? Ja miks on sellele nii raske vastata?

Selgub, et asünkroonsete funktsioonide mõistmine nõuab palju teadmisi JavaScripti põhimõttelise toimimise kohta.

Vaatame seda kontseptsiooni ja õpime selle käigus palju JavaScripti kohta.

Oled sa valmis? Lähme.

Mis on asünkroonne kood?

Kujunduse järgi on JavaScript sünkroonne programmeerimiskeel. See tähendab, et kui kood on täidetud, algab JavaScript faili ülaosast ja läbib koodi rea kaupa, kuni see on valmis.

Selle disaini otsuse tulemus on see, et korraga võib juhtuda ainult üks asi.

Võite mõelda sellele, nagu žongleeriksite kuue väikese palliga. Žongleerimise ajal on su käed hõivatud ega saa millegi muuga hakkama.

Sama on ka JavaScripti puhul: kui kood töötab, on tal selle koodiga käed täis. Me nimetame seda sellist sünkroonse koodi blokeerimiseks . Sest see blokeerib tõhusalt muu koodi käitamise.

Ringi tagasi žongleerimise näite juurde. Mis juhtuks, kui soovite veel ühe palli lisada? Kuue palli asemel tahtsite žongleerida seitsme palliga. See võib olla probleem.

Sa ei taha žongleerimist lõpetada, sest see on lihtsalt nii lõbus. Kuid ka teist palli ei saa minna, sest see tähendaks, et peate peatuma.

Lahendus? Delegeerige töö sõbrale või pereliikmele. Nad ei žongleeri, nii et nad saavad minna ja saavad teile palli hankida, seejärel visata see žongleerimisse ajal, mil teie käsi on vaba ja olete valmis lisama veel ühe palli keskžongleerimisel.

See on asünkroonne kood. JavaScript delegeerib selle töö millekski muuks ja jätkab siis oma äri. Kui see on valmis, saab see töö tulemused tagasi.

Kes teeb muud tööd?

Hästi, nii et me teame, et JavaScript on sünkroonne ja laisk. Ta ei taha kogu tööd ise teha, nii et ta kasvatab seda millekski muuks.

Kuid kes on see salapärane üksus, mis töötab JavaScripti jaoks? Ja kuidas see palgatakse JavaScripti jaoks tööle?

Vaatame nüüd asünkroonse koodi näidet.

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

Selle koodi käivitamisel tekib konsoolis järgmine väljund:

// in console Hi there Han

Hästi. Mis toimub?

Selgub, et viis, kuidas me JavaScripti abil välja töötame, on keskkonnaspetsiifiliste funktsioonide ja API-de kasutamine. Ja see tekitab JavaScriptis suurt segadust.

JavaScript töötab alati keskkonnas.

Sageli on see keskkond brauser. Kuid see võib olla ka NodeJS-i serveris. Kuid mis on maa peal erinevus?

Erinevus - ja see on oluline - seisneb selles, et brauser ja server (NodeJS) pole funktsionaalsuse mõttes samaväärsed. Nad on sageli sarnased, kuid pole samad.

Illustreerime seda ühe näitega. Oletame, et JavaScript on eepilise fantaasiaraamatu peategelane. Lihtsalt tavaline talulaps.

Oletame nüüd, et see talupoeg leidis kaks spetsiaalsete soomuste ülikonda, mis andsid neile võimu, mis ületaks nende endi oma.

Kui nad kasutasid brauseri soomusrüü, said nad juurdepääsu teatud võimalustele.

Kui nad kasutasid serveri kaitserüü, said nad juurdepääsu veel ühele võimekusele.

Nendel ülikondadel on teatud kattuvus, sest nende ülikondade loojatel olid teatud kohtades samad vajadused, kuid teistes mitte.

Selline on keskkond. Koodi käitamise koht, kus on olemas olemasoleva JavaScripti keele peale ehitatud tööriistad. Need ei ole keele osa, kuid rida on sageli hägune, sest me kasutame neid tööriistu iga päev koodi kirjutades.

setTimeout, fetch ja DOM on kõik veebi API-de näited. (Veebirakenduste täielikku loendit näete siit.) Need on tööriistad, mis on sisse ehitatud brauserisse ja mis on meie koodi käitamisel meile kättesaadavad.

Ja kuna me kasutame JavaScripti alati keskkonnas, näib, et need on osa keelest. Aga ei ole.

Nii et kui olete kunagi mõelnud, miks saate brauseris käivitamisel JavaScripti tõmmata (kuid peate installima paketi, kui käivitate selle NodeJS-is), on see põhjus. Keegi arvas, et tõmbamine on hea mõte ja ehitas selle NodeJS-i keskkonna tööriistaks.

Segane? Jah!

Kuid nüüd saame lõpuks aru, mis JavaScripti tööd võtab ja kuidas see tööle võetakse.

Selgub, et töö võtab just keskkond ja viis, kuidas keskkond seda tööd tegema panna, on keskkonnale kuuluva funktsionaalsuse kasutamine. Näiteks tõmbad või setTimeout brauseri keskkonnas.

Mis teosega juhtub?

Suurepärane. Nii et keskkond võtab selle töö enda kanda. Siis mida?

Mingil hetkel peate tulemused tagasi saama. Mõelgem aga sellele, kuidas see töötaks.

Tuleme tagasi algusest peale žongleerimise näite juurde. Kujutage ette, et palusite uut palli ja üks sõber hakkas just teile palli viskama, kui te polnud selleks valmis.

See oleks katastroof. Võib-olla võiksite saada õnne ja tabada selle ning viia see tõhusalt oma rutiini. Kuid seal on suur võimalus, et see võib põhjustada teie pallide viskamise ja rutiini kukkumise. Kas poleks parem, kui annaksite ranged juhised, millal pall kätte saada?

Nagu selgub, kehtivad JavaScripti delegeeritud tööd vastu võtma ranged reeglid.

Neid reegleid reguleerib sündmuse silmus ja need hõlmavad mikroteguri ja makrotaskide järjekorda. Jah, ma tean. Seda on palju. Aga kannata mind.

Hästi. Nii et kui delegeerime asünkroonse koodi brauserile, võtab brauser koodi ja käitab selle ning võtab selle töökoormuse enda kanda. Kuid brauserile antakse mitu ülesannet, seega peame veenduma, et saame need ülesanded prioriseerida.

Siin tulevad mängu mikrotaskude järjekord ja makrotaskide järjekord. Brauser võtab selle töö ette, teeb selle ära ja asetab siis tulemuse ühte kahest järjekorrast vastavalt vastuvõetava töö tüübile.

Näiteks lubadused paigutatakse mikrotaskude järjekorda ja neil on kõrgem prioriteet.

Sündmused ja setTimeout on näited töödest, mis pannakse makrotaskide järjekorda ja millel on madalam prioriteet.

Nüüd, kui töö on tehtud ja see on paigutatud ühte kahest järjekorrast, jookseb sündmuse silmus edasi-tagasi ja kontrollib, kas JavaScripti on valmis tulemuste saamiseks.

Alles siis, kui JavaScripti kogu sünkroonkood on käivitatud ning see on hea ja valmis, hakkab sündmuste silmus järjekordadest valima ja funktsioone JavaScripti käivitamiseks tagasi andma.

Nii et vaatame ühte näidet:

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

Mis järjekord siin saab olema?

  1. Esiteks delegeeritakse setTimeout brauserile, kes teeb selle töö ära ja paneb saadud funktsiooni makrotask järjekorda.
  2. Teiseks delegeeritakse tõmbamine brauserile, kes selle töö võtab. See otsib andmed lõpp-punktist ja paneb saadud funktsioonid mikrotasandi järjekorda.
  3. Javascript logib "Mis supi" välja?
  4. Sündmuse silmus kontrollib, kas JavaScripti on valmis järjekorras olevate tööde tulemuste saamiseks.
  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:

  • täidetud - toiming edukalt lõpule viidud
  • tagasi lükatud - toiming nurjus
  • ootel - kumbki toiming pole lõpule viidud
  • lahendatud - on täidetud või tagasi lükatud

Lubadus saab lahenduse ja tagasilükkamise funktsiooni, mida saab kutsuda ühe sellise seisundi käivitamiseks.

Lubaduste üks suur müügiargument on see, et saame aheldada funktsioone, mida tahame juhtida edu (lahenduse) või ebaõnnestumise (tagasilükkamise) korral:

  • Funktsiooni registreerimiseks edukaks kasutamiseks kasutame
  • Funktsiooni registreerimiseks rikke korral kasutame .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))

Täiuslik. Vaatame nüüd lähemalt, kuidas see kapoti all välja näeb, kasutades näiteks näidet Fetch:

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 

Nii et saame kasutada lubadusi asünkroonse töö tegemiseks ja olla kindlad, et saame nende lubaduste tulemustega hakkama. See on väärtuspakkumine. Kui soovite lubadustest rohkem teada saada, saate nende kohta lugeda siit ja siit.

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.

Reactiga luuakse ka terve rakendus, kui see on teie moos. Ja ma kavatsen tulevikus siia JavaScripti teemadel põhjalikult lisada palju rohkem sisu.

Ja kui soovite tere öelda või veebiarenduse teemal vestelda, võiksite alati minuga ühendust võtta twitteris aadressil @foseberg. Täname lugemast!