Cum funcționează bucla de evenimente în JavaScript?

Deși scrierea codului de producție la scară largă ar putea cere o înțelegere avansată a limbajelor precum C++ și C, JavaScript permite adesea crearea de coduri funcționale chiar și cu o cunoaștere de bază a posibilităților limbajului.

Concepte precum transmiterea apelurilor inverse către funcții sau crearea codului asincron nu sunt, în general, greu de implementat. Din acest motiv, majoritatea dezvoltatorilor JavaScript nu se preocupă prea mult de mecanismele interne, preferând să ignore complexitatea abstractă oferită de limbaj.

Ca dezvoltator JavaScript, este din ce în ce mai important să înțelegem ce se întâmplă în profunzime și cum funcționează aceste abstractizări. Cunoașterea acestor mecanisme ne poate ajuta să luăm decizii mai bune, care pot duce la creșterea drastică a performanței codului nostru.

Acest articol se concentrează pe un concept crucial, dar adesea insuficient înțeles în JavaScript: BUCLA DE EVENIMENTE!

În JavaScript, scrierea codului asincron este inevitabilă, dar ce înseamnă cu adevărat execuția asincronă a codului? Răspunsul este: Bucla de Evenimente.

Pentru a înțelege funcționarea buclei de evenimente, trebuie să analizăm mai întâi ce este JavaScript și cum funcționează acesta!

Ce este JavaScript?

Înainte de a continua, propun să ne întoarcem la noțiunile fundamentale. Ce este de fapt JavaScript? Putem defini JavaScript ca fiind:

Un limbaj de nivel înalt, interpretat, neblocant, asincron și cu un singur fir de execuție.

Un pic cam tehnic, nu-i așa? 🤔

Haideți să descompunem această definiție!

Cuvintele cheie importante pentru acest articol sunt: cu un singur fir de execuție, neblocant, concurent și asincron.

Un singur fir de execuție

Un fir de execuție reprezintă cea mai mică secvență de instrucțiuni dintr-un program care poate fi gestionată independent de un planificator. Un limbaj de programare cu un singur fir de execuție poate efectua o singură sarcină sau operație la un moment dat. Aceasta înseamnă că va executa un întreg proces de la început până la sfârșit, fără a fi întrerupt sau oprit.

Spre deosebire de limbajele cu mai multe fire de execuție, unde mai multe procese pot rula simultan pe diferite fire, fără a se bloca reciproc.

Dar cum poate fi JavaScript simultan cu un singur fir de execuție și neblocant?

Și ce înseamnă mai exact blocarea?

Neblocant

Nu există o definiție unică a blocării; în general, se referă la operațiuni care rulează lent pe firul de execuție. Așadar, neblocarea se referă la operațiuni care nu sunt lente pe firul de execuție.

Am menționat că JavaScript rulează pe un singur fir de execuție, dar este și neblocant, ceea ce înseamnă că sarcinile rulează rapid pe stiva de apeluri. Cum este posibil acest lucru? Ce se întâmplă când folosim cronometre sau bucle?

Calm! Vom afla în curând 😉.

Concurent

Concurența se referă la capacitatea codului de a fi executat simultan de mai multe fire.

Acum devine interesant. Cum poate fi JavaScript simultan cu un singur fir de execuție și concurent? Adică, cum poate executa codul cu mai mult de un singur fir?

Asincron

Programarea asincronă implică rularea codului în cadrul unei bucle de evenimente. Când apare o operație de blocare, un eveniment este inițiat. Codul de blocare continuă să ruleze fără a bloca firul de execuție principal. Când codul de blocare se termină, rezultatul operațiilor de blocare este trimis înapoi în stivă.

Dar, nu am spus că JavaScript are un singur fir de execuție? Atunci ce execută codul de blocare în timp ce permite altor coduri din fir să fie executate?

Înainte de a continua, să recapitulăm punctele de mai sus:

  • JavaScript are un singur fir de execuție.
  • JavaScript este neblocant, adică procesele lente nu blochează execuția sa.
  • JavaScript este concurent, adică execută codul pe mai multe fire în același timp.
  • JavaScript este asincron, adică rulează codul de blocare în altă parte.

Aceste afirmații nu par să se lege logic. Cum poate un limbaj cu un singur fir de execuție să fie simultan neblocant, concurent și asincron?

Să aprofundăm puțin, să analizăm motoarele runtime JavaScript, cum ar fi V8. Poate că există fire ascunse de care nu știm.

Motorul V8

Motorul V8 este un motor de execuție web open-source de înaltă performanță pentru JavaScript, scris în C++ de Google. Majoritatea browserelor folosesc motorul V8 pentru a rula JavaScript, iar popularul mediu de execuție Node.js îl utilizează și el.

Simplu spus, V8 este un program C++ care primește cod JavaScript, îl compilează și îl execută.

V8 îndeplinește două funcții principale:

  • Alocarea memoriei heap
  • Contextul de execuție al stivei de apeluri

Din păcate, suspiciunile noastre sunt nefondate. V8 are o singură stivă de apeluri. Gândiți-vă la stiva de apeluri ca fiind firul de execuție.

Un fir === o stivă de apeluri === o execuție la un moment dat.

Imagine – Hacker Noon

Deoarece V8 are o singură stivă de apeluri, cum rulează JavaScript concomitent și asincron, fără a bloca firul de execuție principal?

Să încercăm să aflăm scriind un cod asincron simplu, dar comun și să-l analizăm împreună.

JavaScript rulează fiecare linie de cod în ordine, una după alta (cu un singur fir de execuție). Așa cum era de așteptat, prima linie este afișată în consolă. Dar de ce este afișată ultima linie înaintea codului de timeout? De ce procesul de execuție nu așteaptă codul de timeout (blocare) înainte de a continua cu rularea ultimei linii?

Se pare că un alt fir ne-a ajutat să executăm acel timeout, deoarece suntem destul de siguri că un fir poate executa o singură sarcină la un moment dat.

Să aruncăm o scurtă privire în codul sursă V8 pentru un moment.

Ce???!!! Nu există funcții de cronometru în V8, nici DOM? Nici evenimente? Fără AJAX?… Da!!!

Evenimentele, DOM, temporizatoarele etc. nu fac parte din implementarea de bază a JavaScript. JavaScript se conformează strict specificațiilor EcmaScript, iar diferitele versiuni ale sale sunt adesea menționate în funcție de specificațiile lor EcmaScript (ES X).

Fluxul de lucru de execuție

Evenimentele, temporizatoarele și solicitările Ajax sunt furnizate de browsere și sunt numite adesea API-uri web. Ele permit JavaScript-ului cu un singur fir de execuție să fie neblocant, concurent și asincron! Dar cum?

Fluxul de lucru de execuție al oricărui program JavaScript este format din trei secțiuni majore: stiva de apeluri, API-ul web și coada de sarcini.

Stiva de apeluri

O stivă este o structură de date în care ultimul element adăugat este întotdeauna primul care este scos din stivă. Gândiți-vă la o stivă de farfurii, unde numai ultima farfurie adăugată poate fi îndepărtată prima. O stivă de apeluri nu este altceva decât o structură de date tip stivă în care sarcinile sau codul sunt executate în consecință.

Să analizăm exemplul de mai jos:

Sursa – https://youtu.be/8aGhZQkoFbQ

Când apelăm funcția `printSquare()`, aceasta este introdusă în stiva de apeluri. Funcția `printSquare()` apelează funcția `square()`. Funcția `square()` este introdusă în stivă și apelează la rândul său funcția `multiplicare()`. Funcția `multiplicare` este introdusă în stivă. Deoarece funcția `multiplicare` se termină prima și este ultimul element introdus în stivă, este rezolvată prima și este eliminată din stivă, urmată de funcția `square()` și apoi de funcția `printSquare()`.

API-ul web

Aici este executat codul care nu este gestionat de motorul V8, pentru a nu „bloca” firul de execuție principal. Când stiva de apeluri întâlnește o funcție API web, procesul este imediat transferat către API-ul web, unde este executat, eliberând astfel stiva de apeluri pentru a efectua alte operațiuni în timpul execuției sale.

Să revenim la exemplul nostru `setTimeout` de mai sus:

Când rulăm codul, prima linie `console.log` este introdusă în stivă și obținem rezultatul aproape imediat. Când ajungem la timeout, cronometrele sunt gestionate de browser și nu fac parte din implementarea de bază a V8. Acesta este trimis la API-ul web, eliberând stiva, astfel încât aceasta să poată efectua alte operațiuni.

În timp ce timeout-ul rulează, stiva trece la următoarea linie de cod și rulează ultimul `console.log`, ceea ce explică de ce îl obținem înaintea rezultatului temporizatorului. Odată ce cronometrul se termină, se întâmplă ceva. `console.log` din cadrul temporizatorului reapare ca prin magie în stiva de apeluri!

Cum?

Bucla de evenimente

Înainte de a discuta despre bucla de evenimente, să trecem mai întâi prin funcția cozii de sarcini.

Revenind la exemplul nostru de timeout, odată ce API-ul web termină de executat sarcina, nu o trimite automat înapoi în stiva de apeluri. În schimb, o trimite în coada de sarcini.

O coadă este o structură de date care funcționează pe principiul „primul intrat, primul ieșit”, astfel încât sarcinile sunt introduse în coadă și ies în aceeași ordine. Activitățile care au fost executate de API-urile web sunt introduse în coada de activități și apoi revin la stiva de apeluri pentru a afișa rezultatul.

Dar stați. Ce este, de fapt, bucla de evenimente???

Sursa – https://youtu.be/8aGhZQkoFbQ

Bucla de evenimente este un proces care așteaptă ca stiva de apeluri să fie goală înainte de a introduce apelurile înapoi din coada de activități în stiva de apeluri. Odată ce stiva este goală, bucla de evenimente se activează și verifică coada de sarcini pentru apeluri disponibile. Dacă există, le introduce în stiva de apeluri, așteaptă ca stiva de apeluri să fie din nou goală și repetă același proces.

Sursa – https://www.quora.com/How-does-an-event-loop-work/answer/Timothy-Maxwell

Diagrama de mai sus demonstrează fluxul de lucru de bază dintre bucla de evenimente și coada de activități.

Concluzie

Deși aceasta este o introducere simplificată, conceptul de programare asincronă în JavaScript oferă o perspectivă clară asupra a ceea ce se întâmplă în profunzime și cum JavaScript reușește să ruleze simultan și asincron cu un singur fir de execuție.

JavaScript este un limbaj foarte popular și, dacă sunteți curios să aprofundați cunoștințele, vă recomand să aruncați o privire la acest curs Udemy.