Când se termină o funcție asincronă? Și de ce este o întrebare atât de greu de răspuns?

Se pare că înțelegerea funcțiilor asincrone necesită o mulțime de cunoștințe despre modul în care funcționează fundamental JavaScript.

Să explorăm acest concept și să învățăm multe despre JavaScript în acest proces.

Sunteți gata? Sa mergem.

Ce este codul asincron?

Prin proiectare, JavaScript este un limbaj de programare sincron. Aceasta înseamnă că, atunci când codul este executat, JavaScript începe în partea de sus a fișierului și rulează prin cod linie cu linie, până când se termină.

Rezultatul acestei decizii de proiectare este că se poate întâmpla un singur lucru în același timp.

Vă puteți gândi la asta ca și cum ați jongla cu șase bile mici. În timp ce jonglezi, mâinile tale sunt ocupate și nu se pot descurca cu nimic altceva.

Este la fel cu JavaScript: odată ce codul rulează, acesta are mâinile pline cu codul respectiv. Numim acest tip de cod sincron blocare. Deoarece blochează efectiv alte coduri.

Să ne întoarcem la exemplul de jonglerie. Ce s-ar întâmpla dacă ai vrea să adaugi o altă minge? În loc de șase bile, ai vrut să jonglezi cu șapte bile. Asta ar putea fi o problemă.

Nu vrei să te oprești din jonglerie, pentru că este atât de distractiv. Dar nici nu poți merge să iei o altă minge, pentru că asta ar însemna că ar trebui să te oprești.

Soluția? Delegați munca unui prieten sau unui membru al familiei. Nu jonglează, așa că pot merge să ia mingea pentru dvs., apoi aruncați-o în jonglerie într-un moment în care mâna este liberă și sunteți gata să adăugați o altă bilă la mijlocul jonglei.

Acesta este codul asincron. JavaScript delegă lucrarea către altceva, apoi se ocupă de propria afacere. Apoi, când este gata, va primi rezultatele din lucrare.

Cine face cealaltă muncă?

Bine, deci știm că JavaScript este sincron și leneș. Nu vrea să facă toată munca în sine, așa că o transformă în altceva.

Dar cine este această entitate misterioasă care lucrează pentru JavaScript? Și cum este angajat să funcționeze pentru JavaScript?

Ei bine, să aruncăm o privire la un exemplu de cod asincron.

const logName = () => {
   console.log("Han")
}

setTimeout(logName, 0)

console.log("Hi there")

Rularea acestui cod are ca rezultat următoarea ieșire în consolă:

// in console
Hi there
Han

Bine. Ce se întâmplă?

Se pare că modul în care cultivăm funcționează în JavaScript este de a utiliza funcții și API-uri specifice mediului. Și aceasta este o sursă de mare confuzie în JavaScript.

JavaScript rulează întotdeauna într-un mediu.

Adesea, acel mediu este browserul. Dar poate fi și pe server cu NodeJS. Dar ce pământ este diferența?

Diferența – și acest lucru este important – este că browserul și serverul (NodeJS), funcțional, nu sunt echivalente. Ele sunt adesea similare, dar nu sunt la fel.

Să ilustrăm acest lucru cu un exemplu. Să presupunem că JavaScript este protagonistul unei cărți fantastice epice. Doar un copil de fermă obișnuit.

Acum, să spunem că acest copil de fermă a găsit două costume de armură specială care le-au dat puteri dincolo de ale lor.

Când au folosit armura browserului, au obținut acces la un anumit set de capabilități.

Când au folosit armura serverului, au obținut acces la un alt set de capabilități.

Aceste costume au unele suprapuneri, deoarece creatorii acestor costume aveau aceleași nevoi în anumite locuri, dar nu și în altele.

Aceasta este ceea ce este un mediu. Un loc unde se rulează codul, unde există instrumente care sunt construite deasupra limbajului JavaScript existent. Ele nu fac parte din limbaj, dar linia este adesea estompată deoarece folosim aceste instrumente în fiecare zi când scriem cod.

setTimeout, aduc, și DOM sunt toate exemple de API-uri web. (Poti vezi lista completă a API-urilor web aici.) Sunt instrumente încorporate în browser și care ne sunt puse la dispoziție atunci când codul nostru este rulat.

Și pentru că rulăm întotdeauna JavaScript într-un mediu, se pare că acestea fac parte din limbaj. Dar ele nu sunt.

Deci, dacă v-ați întrebat vreodată de ce puteți folosi fetch în JavaScript când îl rulați în browser (dar trebuie să instalați un pachet când îl rulați în NodeJS), acesta este motivul. Cineva a considerat că preluarea este o idee bună și a construit-o ca un instrument pentru mediul NodeJS.

Confuz? Da!

Dar acum putem înțelege în cele din urmă ce presupune lucrul din JavaScript și cum este angajat.

Se pare că mediul este cel care preia munca și modul de a face ca mediul să facă acea muncă este de a utiliza funcționalitatea care aparține mediului. De exemplu aduc sau setTimeout în mediul browserului.

Ce se întâmplă cu munca?

Grozav. Deci mediul își asumă munca. Atunci ce?

La un moment dat trebuie să obțineți rezultatele înapoi. Dar să ne gândim cum ar funcționa acest lucru.

Să revenim la exemplul de jonglerie de la început. Imaginați-vă că ați cerut o minge nouă și un prieten tocmai a început să vă arunce mingea când nu erați pregătit.

Ar fi un dezastru. Poate ai putea avea noroc, să-l prinzi și să-l introduci în rutina ta eficient. Dar există mari șanse ca aceasta să vă determine să renunțați la toate bilele și să vă prăbușiți rutina. Nu ar fi mai bine dacă ai da instrucțiuni stricte cu privire la momentul primirii mingii?

După cum se dovedește, există reguli stricte referitoare la momentul în care JavaScript poate primi lucrări delegate.

Aceste reguli sunt guvernate de bucla evenimentului și implică coada de microtask și macrotask. Da, stiu. E mult. Dar suportă-mă.

Tutorial Async Await JavaScript Cum sa asteptati finalizarea unei

Bine. Deci, atunci când delegăm codul asincron browserului, browserul preia și rulează codul și preia sarcina de lucru respectivă. Dar pot exista mai multe sarcini care sunt date browserului, deci trebuie să ne asigurăm că putem acorda prioritate acestor sarcini.

Aici intervin coada de microtask și coada de macrotask. Browserul va prelua lucrarea, o va face, apoi va plasa rezultatul într-una din cele două cozi în funcție de tipul de lucru pe care îl primește.

Promisiunile, de exemplu, sunt plasate în coada de microtare și au o prioritate mai mare.

Evenimente și setTimeout sunt exemple de lucru care sunt plasate în coada de macrotask și au o prioritate mai mică.

Acum, odată ce lucrarea este terminată și este plasată într-una din cele două cozi, bucla evenimentului va rula înainte și înapoi și va verifica dacă JavaScript este sau nu gata să primească rezultatele.

Numai când JavaScript este terminat, rulând tot codul său sincron și este bun și gata, bucla evenimentului va începe să selecteze din cozi și să predea funcțiile înapoi la JavaScript pentru a rula.

Deci, să aruncăm o privire la un exemplu:

setTimeout(() => console.log("hello"), 0) 

fetch("https://someapi/data").then(response => response.json())
                             .then(data => console.log(data))

console.log("What soup?")

Care va fi comanda aici?

  1. În primul rând, setTimeout este delegat browserului, care face treaba și pune funcția rezultată în coada de macrotask.
  2. În al doilea rând, preluarea este delegată browserului, care preia lucrarea. Acesta preia datele de la punctul final și pune funcțiile rezultate în coada de microtare.
  3. Javascript se deconectează „Ce supă”?
  4. Bucla de evenimente verifică dacă JavaScript este sau nu gata să primească rezultatele din lucrarea din coadă.
  5. Când consola log este terminată, JavaScript este gata. Bucla de evenimente alege funcțiile din coada de așteptare din coada de microtare, care are o prioritate mai mare și le dă înapoi la JavaScript pentru a le executa.
  6. După ce coada de microtare este goală, callback-ul setTimeout este scos din coada de macrotask și redat JavaScript pentru a fi executat.
In console:
// What soup?
// the data from the api
// hello

Promisiuni

Acum ar trebui să aveți o mulțime de cunoștințe despre modul în care codul asincron este tratat de JavaScript și de mediul browserului. Deci, să vorbim despre promisiuni.

O promisiune este o construcție JavaScript care reprezintă o valoare necunoscută în viitor. Conceptual, o promisiune este doar JavaScript care promite să revină o valoare. Poate fi rezultatul unui apel API sau poate fi un obiect de eroare dintr-o cerere de rețea eșuată. Ai garanția că vei obține ceva.

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)
})

O promisiune poate avea următoarele stări:

  • îndeplinită – acțiune finalizată cu succes
  • respins – acțiunea a eșuat
  • în așteptare – nici o acțiune nu a fost finalizată
  • stabilit – a fost îndeplinit sau respins

O promisiune primește o funcție de hotărâre și de respingere care poate fi apelată pentru a declanșa una dintre aceste stări.

Unul dintre marile puncte de vânzare ale promisiunilor este că putem înlănțui funcții pe care dorim să le îndeplinim în urma succesului (rezolvării) sau eșecului (respingerii):

  • Pentru a înregistra o funcție care să ruleze cu succes folosim .then
  • Pentru a înregistra o funcție care rulează în caz de eșec, folosim .catch
// Fetch returns a promise
fetch("https://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("https://swapi.dev/api/people/1")
	.then((res) => doSomethingWithResult(res))
    .then((finalResult) => console.log(finalResult))
    .catch((err => doSomethingWithErr(err))

Perfect. Acum să aruncăm o privire mai atentă la cum arată acest lucru sub capotă, folosind fetch ca exemplu:

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("https://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
  
  

Deci, putem folosi promisiunile pentru a face o muncă asincronă și pentru a fi siguri că putem gestiona orice rezultat din aceste promisiuni. Aceasta este propunerea de valoare. Dacă doriți să aflați mai multe despre promisiuni, puteți citi mai multe despre ele aici și aici.

Când folosim promisiuni, ne înlănțuim funcțiile pe promisiunea de a gestiona diferitele scenarii.

Acest lucru funcționează, dar trebuie totuși să ne ocupăm de logica noastră în apelurile de apel (funcții imbricate) odată ce ne obținem rezultatele înapoi. Ce se întâmplă dacă am putea folosi promisiuni, dar să scriem cod sincron? Se pare că putem.

Async / Await

Async / Await este un mod de a scrie promisiuni care ne permite să scriem cod asincron într-un mod sincron. Haideți să aruncăm o privire.

const getData = async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
    const data = await response.json()
    
    console.log(data)
}

getData()

Aici nu s-a schimbat nimic sub capotă. Încă folosim promisiuni de a prelua date, dar acum arată sincron și nu mai avem blocuri .then și .catch.

Async / Await este de fapt doar zahăr sintactic, oferind o modalitate de a crea cod mai ușor de argumentat, fără a schimba dinamica de bază.

Să aruncăm o privire la modul în care funcționează.

Async / Await ne permite să folosim generatoare la pauză executarea unei funcții. Când folosim async / await nu blocăm, deoarece funcția redă controlul către programul principal.

Atunci când promisiunea se rezolvă, folosim generatorul pentru a reda controlul funcției asincrone cu valoarea din promisiunea rezolvată.

Puteți citi mai multe aici pentru o imagine de ansamblu excelentă de generatoare și cod asincron.

De fapt, putem scrie acum cod asincron care arată ca un cod sincron. Ceea ce înseamnă că este mai ușor să argumentăm și că putem folosi instrumente sincrone pentru gestionarea erorilor, cum ar fi try / catch:

const getData = async () => {
    try {
    	const response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
    	const data = await response.json()
        console.log(data)
    } catch (err) {
       console.log(err)
    }
    
}

getData()

Bine. Deci, cum îl folosim? Pentru a utiliza async / await, trebuie să înaintăm funcția cu async. Aceasta nu o face o funcție asincronă, ci doar ne permite să folosim wait în interiorul ei.

Dacă nu furnizați cuvântul cheie asincronizat, va rezulta o eroare de sintaxă atunci când încercați să utilizați wait în interiorul unei funcții obișnuite.

const getData = async () => {
	console.log("We can use await in this function")
}

Din această cauză, nu putem folosi codul async / await la nivel superior. Dar asincronizarea și așteptarea sunt încă doar zahăr sintactic decât promisiunile. Deci, putem face față cazurilor de nivel superior cu un lanț de promisiuni:

async function getData() {
  let response = await fetch('http://apiurl.com');
}

// getData is a promise
getData().then(res => console.log(res)).catch(err => console.log(err); 

Acest lucru expune un alt fapt interesant despre asincronizare / așteptare. Când definiți o funcție ca asincronă, va da întotdeauna o promisiune.

Utilizarea async / await poate părea la început magie. Dar, ca orice magie, este doar o tehnologie suficient de avansată care a evoluat de-a lungul anilor. Sperăm că acum aveți o înțelegere solidă a elementelor fundamentale și puteți folosi asincronizarea / așteptați cu încredere.

Concluzie

Dacă ai ajuns aici, felicitări. Tocmai ați adăugat un set cheie de cunoștințe despre JavaScript și modul în care acesta funcționează cu mediile sale în cutia dvs. de instrumente.

Acesta este cu siguranță un subiect confuz, iar liniile nu sunt întotdeauna clare. Dar acum, sperăm, să înțelegeți cum funcționează JavaScript cu codul asincron în browser și o înțelegere mai puternică atât asupra promisiunilor, cât și asupra asincronizării / așteptării.

Dacă ți-a plăcut acest articol, s-ar putea să te bucuri și de al meu Canalul canalului YouTube. În prezent am un serii fundamentale web mergând pe unde trec HTTP, construirea de servere web de la zero și altele.

Există și o serie construirea unei aplicații întregi cu React, dacă acesta este blocajul tău. Și intenționez să adaug aici mult mai mult conținut în viitor, aprofundând subiectele JavaScript.

Și dacă doriți să salutați sau să discutați despre dezvoltarea web, ați putea să vă contactați întotdeauna pe twitter la @foseberg. Mulțumesc pentru lectură!