Async Await JavaScript Tutorial - Kaip laukti, kol funkcija baigsis JS

Kada baigiasi asinchroninė funkcija? Ir kodėl į šį klausimą sunku atsakyti?

Na, pasirodo, kad norint suprasti asinchronines funkcijas reikia daug žinių apie tai, kaip „JavaScript“ veikia iš esmės.

Panagrinėkime šią koncepciją ir daug sužinokime apie „JavaScript“.

Ar tu pasiruošęs? Eime.

Kas yra asinchroninis kodas?

Pagal konstrukciją „JavaScript“ yra sinchroninė programavimo kalba. Tai reiškia, kad kai vykdomas kodas, „JavaScript“ prasideda failo viršuje ir eina per kodą eilutė po eilutės, kol tai bus padaryta.

Šio dizaino sprendimo rezultatas yra tai, kad vienu metu gali atsitikti tik vienas dalykas.

Galite tai pagalvoti taip, tarsi žongliruotumėte šešiais mažais kamuoliukais. Kol žongliruoji, tavo rankos užimtos ir negali susitvarkyti su niekuo kitu.

Lygiai taip pat ir su „JavaScript“: kai kodas veikia, jis turi visas rankas su tuo kodu. Mes tai vadiname tokiu sinchroninio kodo blokavimu . Nes tai veiksmingai blokuoja kito kodo paleidimą.

Grįžkime prie žongliravimo pavyzdžio. Kas nutiktų, jei norėtumėte pridėti dar vieną kamuolį? Vietoj šešių kamuoliukų norėjote žongliruoti septyniais kamuoliais. Tai gali būti problema.

Nenorite nustoti žongliruoti, nes tai tiesiog labai smagu. Bet jūs negalite eiti ir gauti kito kamuolio, nes tai reikštų, kad turėtumėte sustoti.

Sprendimas? Paveskite darbą draugui ar šeimos nariui. Jie nėra žongliravę, todėl gali nueiti ir pasiimti kamuolį už jus, tada mesti jį į žongliravimą tuo metu, kai ranka yra laisva ir esate pasirengęs pridėti dar vieną kamuolį vidurio žongliru.

Tai yra asinchroninis kodas. „JavaScript“ deleguoja darbą kitam reikalui, o tada užsiima savo verslu. Tada, kai ji bus paruošta, ji grąžins darbo rezultatus.

Kas dirba kitą darbą?

Gerai, todėl žinome, kad „JavaScript“ yra sinchroniškas ir tingus. Ji nenori atlikti viso darbo pati, todėl jį išnaudoja kažkam kitam.

Bet kas yra šis paslaptingas subjektas, veikiantis naudojant „JavaScript“? Ir kaip jį samdyti dirbti su „JavaScript“?

Na, pažvelkime į asinchroninio kodo pavyzdį.

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

Paleidus šį kodą, konsolėje gaunamas toks išvestis:

// in console Hi there Han

Gerai. Kas vyksta?

Pasirodo, kad tai, kaip mes dirbame „JavaScript“, yra naudoti aplinkosaugos funkcijas ir API. Tai kelia didelę painiavą „JavaScript“.

„JavaScript“ visada veikia aplinkoje.

Dažnai ta aplinka yra naršyklė. Bet tai gali būti ir serveryje su „NodeJS“. Bet kuo žemėje yra skirtumas?

Skirtumas - ir tai yra svarbu - yra tai, kad naršyklė ir serveris („NodeJS“) funkcionalumo požiūriu nėra lygiaverčiai. Jie dažnai yra panašūs, tačiau jie nėra vienodi.

Paaiškinkime tai pavyzdžiu. Tarkime, „JavaScript“ yra epinės fantastinės knygos veikėjas. Tiesiog paprastas ūkio vaikas.

Tarkime, kad šis ūkio vaikas rado du specialių šarvų kostiumus, kurie suteikė jiems galių ne tik jų pačių.

Kai jie naudojo naršyklės šarvus, jie gavo prieigą prie tam tikrų galimybių.

Kai jie naudojo serverio šarvus, jie gavo prieigą prie kitų galimybių rinkinių.

Šie kostiumai šiek tiek sutampa, nes šių kostiumų kūrėjai tam tikrose vietose turėjo tuos pačius poreikius, bet kitose - ne.

Tokia yra aplinka. Vieta, kurioje vykdomas kodas, kur yra įrankių, sukurtų ant esamos „JavaScript“ kalbos. Jie nėra kalbos dalis, tačiau eilutė dažnai būna neryški, nes rašydami kodą šias priemones naudojame kiekvieną dieną.

„setTimeout“, „fetch“ ir DOM yra visi žiniatinklio API pavyzdžiai. (Čia galite pamatyti visą žiniatinklio API sąrašą.) Tai yra įrankiai, kurie yra įmontuoti naršyklėje ir kurie mums prieinami paleidus kodą.

Kadangi mes visada naudojame „JavaScript“ aplinkoje, atrodo, kad tai yra kalbos dalis. Bet jie nėra.

Taigi, jei kada nors susimąstėte, kodėl galite naudoti „JavaScript“, kai ją paleidžiate naršyklėje (bet reikia įdiegti paketą, kai ją paleidžiate „NodeJS“), štai kodėl. Kažkas manė, kad „fetch“ yra gera idėja, ir sukūrė ją kaip „NodeJS“ aplinkos įrankį.

Paini? Taip!

Bet dabar mes pagaliau galime suprasti, kas perima „JavaScript“ darbą ir kaip jis pasamdomas.

Pasirodo, kad aplinka imasi darbo, o būdas priversti aplinką tą darbą atlikti yra naudoti funkciją, kuri priklauso aplinkai. Pavyzdžiui, „ fetch“ arba „ setTimeout “ naršyklės aplinkoje.

Kas nutinka kūriniui?

Puiku. Taigi aplinka imasi darbo. Kas tada?

Tam tikru momentu reikia susigrąžinti rezultatus. Bet pagalvokime, kaip tai veiktų.

Grįžkime prie žongliravimo pavyzdžio nuo pat pradžių. Įsivaizduokite, kad paprašėte naujo kamuolio, o draugas jums pradėjo mėtyti kamuolį, kai nebuvote pasirengęs.

Tai būtų katastrofa. Galbūt jums pasiseks ir pagausite jį efektyviai. Tačiau yra didelė tikimybė, kad dėl to gali numesti visus kamuolius ir sugadinti savo rutiną. Ar ne geriau, jei duotumėte griežtus nurodymus, kada gauti kamuolį?

Kaip paaiškėja, yra griežtos taisyklės, kai „JavaScript“ gali gauti deleguotą darbą.

Šias taisykles reguliuoja įvykio ciklas ir jos apima mikrotvarkos ir makrotaskos eilę. Taip, aš žinau. Tai daug. Bet pakentėk mane.

Gerai. Taigi, kai mes perduodame asinchroninį kodą naršyklei, naršyklė paima ir paleidžia kodą bei prisiima tą darbo krūvį. Tačiau gali būti kelios užduotys, kurios pateikiamos naršyklei, todėl turime įsitikinti, kad galime nustatyti šių užduočių prioritetus.

Čia žaidžiamos mikrobangų ir makrotaskų eilės. Naršyklė paims darbą, atliks tai, tada įdės rezultatą į vieną iš dviejų eilučių, atsižvelgdama į gaunamo darbo tipą.

Pavyzdžiui, pažadai dedami į mikrotaskų eilę ir turi didesnį prioritetą.

Įvykiai ir „setTimeout“ yra darbo, įdėto į makrotaskų eilę, pavyzdžiai, kuriems suteikiamas mažesnis prioritetas.

Dabar, kai darbas bus atliktas ir jis bus patalpintas vienoje iš dviejų eilių, įvykio ciklas eis pirmyn ir atgal ir patikrins, ar „JavaScript“ yra pasirengusi gauti rezultatus.

Tik baigus „JavaScript“ paleisti visą sinchroninį kodą, jis yra tinkamas ir paruoštas, įvykių ciklas pradės rinktis iš eilių ir grąžins funkcijas atgal į „JavaScript“, kad būtų paleista.

Taigi pažvelkime į pavyzdį:

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

Kokia čia bus tvarka?

  1. Pirma, „setTimeout“ yra pavesta naršyklei, kuri atlieka darbą ir gautą funkciją įtraukia į makroveiksmo eilę.
  2. Antra, „iškvietimas“ pavedamas naršyklei, kuri perima darbą. Jis nuskaito duomenis iš galutinio taško ir gautas funkcijas įdeda į mikrotvarkos eilę.
  3. „Javascript“ atsijungia nuo „kokios sriubos“?
  4. Įvykių ciklas patikrina, ar „JavaScript“ yra pasirengusi gauti eilės darbo rezultatus.
  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.

Taip pat yra serija, kuria visą programą su „React“, jei tai yra jūsų uogienė. Ateityje planuoju įdėti daug daugiau turinio, gilindamasis į „JavaScript“ temas.

O jei norite pasisveikinti ar paplepėti apie interneto plėtrą, visada galite susisiekti su manimi „Twitter“ adresu @foseberg. Ačiū, kad skaitėte!