Async odota JavaScript-opetusohjelma - Kuinka odottaa toiminnon päättymistä JS: ssä

Milloin asynkroninen toiminto päättyy? Ja miksi tähän on niin vaikea vastata?

On käynyt ilmi, että asynkronisten toimintojen ymmärtäminen vaatii paljon tietoa siitä, miten JavaScript toimii periaatteessa.

Mennään tutkia tätä käsitettä ja oppia paljon JavaScriptistä prosessin aikana.

Oletko valmis? Mennään.

Mikä on asynkroninen koodi?

Suunnittelun mukaan JavaScript on synkroninen ohjelmointikieli. Tämä tarkoittaa, että kun koodi suoritetaan, JavaScript alkaa tiedoston yläosasta ja kulkee koodin läpi rivi riviltä, ​​kunnes se on valmis.

Tämän suunnittelupäätöksen tulos on, että vain yksi asia voi tapahtua kerralla.

Voit ajatella tätä ikään kuin jongleeraat kuusi pientä palloa. Jongleeraamisen aikana kätesi ovat varattuja eivätkä pysty käsittelemään mitään muuta.

Sama on JavaScriptin kanssa: kun koodi on käynnissä, sillä on kädet täynnä kyseistä koodia. Kutsumme tätä tällaiseksi synkroniseksi koodin estoksi . Koska se estää tehokkaasti muun koodin suorittamisen.

Kierretäänpä takaisin jongleerausesimerkkiin. Mitä tapahtuisi, jos haluat lisätä toisen pallon? Kuuden pallon sijasta halusit taitaa seitsemää palloa. Se voi olla ongelma.

Et halua lopettaa jongleerausta, koska se on vain niin hauskaa. Mutta et voi myöskään hankkia toista palloa, koska se tarkoittaisi, että sinun on lopetettava.

Ratkaisu? Siirrä työ ystävälle tai perheenjäsenelle. He eivät ole jongleerausta, joten he voivat mennä hakemaan pallon puolestasi, sitten heittää sen jonglööriisi silloin, kun kätesi on vapaa ja olet valmis lisäämään toisen pallon keskellä.

Tämä on asynkroninen koodi. JavaScript delegoi työn jollekin muulle ja jatkaa sitten omaa liiketoimintaa. Sitten kun se on valmis, se saa tulokset takaisin työstä.

Kuka tekee muuta työtä?

Selvä, joten tiedämme, että JavaScript on synkroninen ja laiska. Se ei halua tehdä kaikkea työtä itse, joten se kasvattaa sitä muulle.

Mutta kuka on tämä salaperäinen kokonaisuus, joka toimii JavaScriptillä? Ja miten se palkataan toimimaan JavaScriptiä varten?

Katsotaanpa nyt esimerkki asynkronisesta koodista.

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

Tämän koodin suorittaminen johtaa seuraavaan tulokseen konsolissa:

// in console Hi there Han

Hyvä on. Mitä tapahtuu?

On käynyt ilmi, että tapa, jolla viljellään työtä JavaScriptissä, on käyttää ympäristökohtaisia ​​toimintoja ja sovellusliittymiä. Ja tämä aiheuttaa suurta hämmennystä JavaScriptissä.

JavaScript toimii aina ympäristössä.

Usein kyseinen ympäristö on selain. Mutta se voi olla myös palvelimella NodeJS: n kanssa. Mutta mikä ihmeessä on ero?

Ero - ja tämä on tärkeää - on se, että selain ja palvelin (NodeJS) eivät ole toiminnallisuuden suhteen samanarvoisia. Ne ovat usein samanlaisia, mutta ne eivät ole samoja.

Valaistaan ​​tätä esimerkillä. Sanotaan, että JavaScript on eeppisen fantasiakirjan päähenkilö. Vain tavallinen maatilapoika.

Sanotaan nyt, että tämä maatilapoika löysi kaksi erikoishaarniskaa, jotka antoivat heille voimaa, joka ylitti heidän omat.

Kun he käyttivät selaimen panssaria, he saivat pääsyn tiettyihin ominaisuuksiin.

Kun he käyttivät palvelimen panssaripukua, he saivat pääsyn toiseen sarjaan ominaisuuksia.

Näillä puvuilla on jonkin verran päällekkäisyyksiä, koska näiden pukujen tekijöillä oli samat tarpeet joissakin paikoissa, mutta ei muissa.

Tämä on ympäristö. Paikka, jossa käytetään koodia, jossa on työkaluja, jotka on rakennettu olemassa olevan JavaScript-kielen päälle. Ne eivät ole osa kieltä, mutta rivi on usein epäselvä, koska käytämme näitä työkaluja joka päivä kirjoittaessamme koodia.

setTimeout, Fetch ja DOM ovat kaikki esimerkkejä Web-sovellusliittymistä. (Näet täydellisen luettelon Web-sovellusliittymistä täältä.) Ne ovat työkaluja, jotka on rakennettu selaimeen ja jotka ovat käytettävissä meille, kun koodimme suoritetaan.

Ja koska käytämme JavaScriptiä aina ympäristössä, näyttää siltä, ​​että nämä ovat osa kieltä. Mutta eivät ole.

Joten jos olet koskaan miettinyt, miksi voit käyttää noutoa Javascriptissa, kun suoritat sen selaimessa (mutta sinun on asennettava paketti, kun suoritat sen NodeJS: ssä), tämä on syy. Joku ajatteli hakemisen olevan hyvä idea ja rakensi sen työkaluksi NodeJS-ympäristölle.

Hämmentävä? Joo!

Mutta nyt voimme vihdoin ymmärtää, mikä ottaa työn JavaScriptistä ja miten se palkataan.

Osoittautuu, että työ on ympäristö, ja tapa saada ympäristö tekemään tämä työ on käyttää ympäristöön kuuluvaa toiminnallisuutta. Esimerkiksi hae tai setTimeout selainympäristössä.

Mitä teokselle tapahtuu?

Loistava. Joten ympäristö ottaa työn vastaan. Mitä sitten?

Jossain vaiheessa sinun on saatava tulokset takaisin. Mutta mietitään, miten tämä toimisi.

Palataan takaisin jongleerausesimerkkiin alusta alkaen. Kuvittele, että pyysit uutta palloa, ja ystäväsi alkoi heittää palloa sinulle, kun et ollut valmis.

Se olisi katastrofi. Ehkä voisit saada onnekas ja tarttua siihen ja saada sen rutiiniin tehokkaasti. Mutta on suuri mahdollisuus, että se voi aiheuttaa sinun pudottaa kaikki pallosi ja kaataa rutiinisi. Eikö olisikin parempi, jos annat tiukat ohjeet siitä, milloin pallo tulee vastaanottaa?

Kuten käy ilmi, on olemassa tiukat säännöt, kun JavaScript voi vastaanottaa delegoitua työtä.

Näitä sääntöjä säätelee tapahtumasilmukka, ja niihin liittyy mikrotehtävä- ja makrotaskujono. Kyllä tiedän. Se on paljon. Mutta kannata minua.

Hyvä on. Joten kun delegoimme asynkronisen koodin selaimelle, selain ottaa ja suorittaa koodin ja ottaa vastaan ​​kyseisen työmäärän. Selaimelle voidaan kuitenkin antaa useita tehtäviä, joten meidän on varmistettava, että voimme priorisoida nämä tehtävät.

Täällä mikrotaskun jono ja makrotason jono tulevat pelaamaan. Selain vie työn, tekee sen ja sijoittaa tuloksen toiseen kahdesta jonosta sen mukaan, minkä tyyppisen työn se vastaanottaa.

Esimerkiksi lupaukset sijoitetaan mikrotaskujonoon ja niillä on korkeampi prioriteetti.

Tapahtumat ja setTimeout ovat esimerkkejä työstä, joka laitetaan makrotask-jonoon ja joilla on matalampi prioriteetti.

Nyt kun työ on tehty ja se on sijoitettu jompaankumpaan jonosta, tapahtumasilmukka kulkee edestakaisin ja tarkistaa, onko JavaScript valmis vastaanottamaan tulokset.

Tapahtumasilmukka alkaa poimia jonoista ja antaa toiminnot takaisin JavaScriptiin suoritettavaksi vasta kun JavaScripti on suoritettu ja synkronoitu koodi on valmis ja valmis.

Joten katsotaanpa esimerkkiä:

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

Mikä järjestys täällä on?

  1. Ensinnäkin setTimeout delegoidaan selaimelle, joka tekee työn ja sijoittaa tuloksena olevan funktion makrotask-jonoon.
  2. Toiseksi nouto delegoidaan selaimelle, joka vie työn. Se hakee tiedot päätepisteestä ja sijoittaa tuloksena olevat toiminnot mikrotehtävän jonoon.
  3. Javascript kirjautuu ulos mitä keittoa?
  4. Tapahtumasilmukka tarkistaa, onko JavaScript valmis vastaanottamaan jonotetun työn tulokset.
  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-sovelluksella rakennetaan myös koko sovellus, jos se on sinun hillosi. Ja aion lisätä tulevaisuudessa paljon enemmän sisältöä tulevaisuudessa perusteellisesti JavaScript-aiheista.

Ja jos haluat sanoa hei tai keskustella verkkokehityksestä, voit aina ottaa yhteyttä minuun twitterissä osoitteessa @foseberg. Kiitos lukemisesta!