Iako razvoj punog produkcijskog koda može zahtevati dubinsko poznavanje programskih jezika kao što su C++ i C, JavaScript se često može pisati uz samo osnovno razumevanje mogućnosti ovog jezika.
Koncepti kao što su prosleđivanje povratnih poziva funkcijama ili pisanje asinhronog koda, često nisu teški za implementaciju. Zato većina JavaScript programera manje brine o tome šta se dešava „ispod haube“. Jednostavno ih ne zanima razumevanje kompleksnosti koju je sam jezik apstrahovao.
Za svakog JavaScript programera, razumevanje šta se zapravo dešava „ispod haube“ i kako većina ovih kompleksnosti, koje su nam apstrahovane, zaista funkcioniše, postaje sve važnije. Ovo nam pomaže da donosimo bolje informisane odluke, što zauzvrat može drastično poboljšati performanse našeg koda.
Ovaj članak se fokusira na jedan od veoma važnih, ali često nerazumljivih koncepata ili termina u JavaScript-u: PETLJU DOGAĐAJA!
Pisanje asinhronog koda je neizbežno u JavaScriptu, ali šta zaista znači kod koji se pokreće asinhrono? Odgovor se krije u petlji događaja.
Pre nego što shvatimo kako funkcioniše petlja događaja, prvo moramo razumeti šta je JavaScript i kako on radi!
Šta je JavaScript?
Pre nego što nastavimo, hajde da se vratimo na same osnove. Šta je zapravo JavaScript? JavaScript bismo mogli definisati kao:
JavaScript je visoko-nivoan, interpretiran, jednonoitni, neblokirajući, asinhroni i konkurentan jezik.
Čekaj, šta je ovo? Knjiška definicija? 🤔
Hajde da je razložimo!
Ključne reči ovde, u kontekstu ovog članka, su jednonoitni, neblokirajući, konkurentan i asinhroni.
Jednonitni (Single-thread)
Nit izvršavanja je najmanja sekvenca programskih instrukcija kojom raspoređivač može upravljati nezavisno. Programski jezik je jednonoitni ako može izvršiti samo jedan zadatak ili operaciju u jednom trenutku. To znači da bi izvršio ceo proces od početka do kraja, bez prekida ili zaustavljanja niti.
Za razliku od jezika sa više niti, gde se više procesa može pokrenuti na nekoliko niti istovremeno, bez međusobnog blokiranja.
Kako JavaScript može biti jednonoitni i neblokirajući u isto vreme?
Ali šta znači blokiranje?
Neblokirajući (Non-blocking)
Ne postoji jedinstvena definicija blokiranja; to jednostavno znači da se stvari sporo odvijaju na niti. Dakle, neblokiranje znači da se stvari ne odvijaju sporo na niti.
Ali čekajte, da li sam rekao da JavaScript radi na jednoj niti? Takođe sam rekao da ne blokira, što znači da se zadatak brzo izvršava na steku poziva? Ali kako??? Šta je sa tim kada pokrenemo tajmere? Petlje?
Opustite se! Saznaćemo za koji trenutak 😉.
Konkurentan
Konkurentnost znači da se kod istovremeno izvršava od strane više od jedne niti.
Dobro, stvari sada postaju stvarno čudne. Kako JavaScript može biti jednonoitni i istovremeno konkurentan? Tj. kako može izvršavati svoj kod sa više od jedne niti?
Asinhroni
Asinhrono programiranje znači da se kod pokreće u petlji događaja. Kada postoji operacija blokiranja, pokreće se događaj. Kod za blokiranje nastavlja da radi bez blokiranja glavne niti izvršavanja. Kada blokirajući kod završi sa radom, rezultat operacije blokiranja stavlja se u red i vraća se nazad u stek.
Ali JavaScript ima samo jednu nit? Šta onda izvršava ovaj kod za blokiranje dok dozvoljava izvršavanje drugog koda na niti?
Pre nego što nastavimo, hajde da rezimiramo gore navedeno:
- JavaScript je jednonoitni.
- JavaScript ne blokira, tj. spori procesi ne blokiraju njegovo izvršavanje.
- JavaScript je konkurentan, tj. izvršava svoj kod na više niti u isto vreme.
- JavaScript je asinhroni, tj. pokreće blokirajući kod negde drugde.
Ali gore navedeno se baš i ne uklapa. Kako jezik sa samo jednom niti može biti neblokirajući, konkurentan i asinhroni?
Zaronimo malo dublje, pogledajmo JavaScript runtime mašinu, V8, možda postoje neke skrivene niti kojih nismo svesni.
V8 Engine
V8 engine je open-source, visoko-performantan engine za izvršavanje Web Assembly i JavaScript koda, napisan u C++ od strane kompanije Google. Većina pretraživača pokreće JavaScript koristeći V8 engine, a koristi ga čak i popularno Node.js okruženje.
Jednostavnim rečima, V8 je C++ program koji prima JavaScript kod, kompajlira ga i izvršava.
V8 radi dve glavne stvari:
- Dodela memorije u heap-u
- Kontekst izvršavanja steka poziva
Nažalost, naša sumnja je bila pogrešna. V8 ima samo jedan stek poziva. Zamislite stek poziva kao nit.
Jedna nit === jedan stek poziva === izvršavanje jedan po jedan.
Pošto V8 ima samo jedan stek poziva, kako onda JavaScript radi konkurentno i asinhrono bez blokiranja glavne izvršne niti?
Pokušajmo da to saznamo pisanjem jednostavnog, ali uobičajenog asinhronog koda i analizirajmo ga zajedno.
JavaScript pokreće svaki red koda red po red, jedan za drugim (jednonoitni). Kao što se i očekivalo, prvi red se ovde ispisuje u konzoli, ali zašto se poslednji red ispisuje pre isteka tajmera? Zašto proces izvršavanja ne sačeka kod isteka (blokiranje) pre nego što nastavi da pokreće poslednji red?
Čini se da nam je neka druga nit pomogla da izvršimo vremensko ograničenje pošto smo prilično sigurni da nit može izvršiti samo jedan zadatak u bilo kom trenutku.
Hajde da na kratko zavirimo u V8 izvorni kod.
Čekaj, šta?! U V8 nema funkcija tajmera, nema DOM-a? Nema događaja? Nema AJAX-a?…. Daaaa!!!
Događaji, DOM, tajmeri itd. nisu deo osnovne implementacije JavaScript-a. JavaScript je striktno usklađen sa specifikacijama ECMAScript-a, a različite njegove verzije se često pominju u skladu sa njegovim ECMAScript specifikacijama (ES X).
Tok izvršavanja
Događaje, tajmere i AJAX zahteve na strani klijenta obezbeđuje pretraživač i oni se često nazivaju Web API. Oni su ti koji omogućavaju da jednonoitni JavaScript bude neblokirajući, konkurentan i asinhroni! Ali kako?
Postoje tri glavna odeljka u toku izvršavanja bilo kog JavaScript programa: stek poziva, Web API i red zadataka.
Stek poziva
Stek je struktura podataka u kojoj je poslednji dodati element uvek prvi koji se uklanja sa steka. Možete ga zamisliti kao gomilu tanjira u kojoj se prvi može ukloniti samo prvi tanjir koji je poslednji dodat. Stek poziva nije ništa drugo do struktura podataka steka u kojoj se zadaci ili kod izvršavaju u skladu s tim.
Pogledajmo sledeći primer:
Izvor – https://youtu.be/8aGhZQKkoFq
Kada pozovete funkciju printSquare(), ona se gura na stek poziva. Funkcija printSquare() poziva funkciju square(). Funkcija square() se gura na stek i takođe poziva funkciju multiply(). Funkcija multiply() se gura na stek. Pošto se funkcija multiply() vraća i poslednja je stvar koja je gurnuta na stek, prvo se rešava i uklanja sa steka, a zatim sledi funkcija square(), a onda funkcija printSquare().
Web API
Ovde se izvršava kod kojim ne upravlja V8 engine, da ne bi „blokirao“ glavnu izvršnu nit. Kada stek poziva naiđe na funkciju Web API-ja, proces se odmah predaje Web API-ju, gde se izvršava i oslobađa stek poziva da obavlja druge operacije tokom njegovog izvršavanja.
Vratimo se na naš primer setTimeOut od gore;
Kada pokrenemo kod, prva linija console.log se gura na stek i skoro odmah dobijamo naš ispis. Kada dođemo do tajmera, tajmerima upravlja pretraživač i nisu deo osnovne implementacije V8. Umesto toga, oni se guraju na Web API, oslobađajući stek kako bi mogao da obavlja druge operacije.
Dok tajmer još uvek traje, stek prelazi na sledeću liniju i pokreće poslednji console.log, što objašnjava zašto ga dobijamo pre ispisa tajmera. Kada se tajmer završi, nešto se dešava. Tajmer console.log se onda magično pojavljuje u grupi poziva!
Kako?
Petlja događaja
Pre nego što razgovaramo o petlji događaja, prvo pogledajmo red zadataka.
Da se vratimo na naš primer sa vremenskim ograničenjem, kada Web API završi sa izvršavanjem zadatka, on ga ne vraća samo nazad na stek poziva automatski. Ide u red zadataka.
Red je struktura podataka koja funkcioniše na principu „prvi ušao, prvi izašao“. Dakle, kada se zadaci guraju u red, oni izlaze istim redosledom. Zadaci koje su izvršili Web API-ji se guraju u red zadataka i zatim se vraćaju na stek poziva da bi se njihov rezultat ispisao.
Ali čekaj. ŠTA JE PETLJA DOGAĐAJA???
Izvor – https://youtu.be/8aGhZQKkoFq
Petlja događaja je proces koji čeka da se stek poziva očisti pre nego što gura povratne pozive iz reda zadataka u stek poziva. Kada se stek očisti, petlja događaja se pokreće i proverava red zadataka za dostupne povratne pozive. Ako ih ima, gura ih na stek poziva, čeka da se stek poziva ponovo očisti i ponavlja isti proces.
Izvor – https://www.quora.com/How-does-an-event-loop-work/answer/Timothy-Maxwell
Gornji dijagram prikazuje osnovni tok rada između petlje događaja i reda zadataka.
Zaključak
Iako je ovo veoma osnovni uvod, koncept asinhronog programiranja u JavaScript-u daje dovoljno uvida da se jasno razume šta se dešava „ispod haube“ i kako JavaScript može da radi konkurentno i asinhrono sa samo jednom niti.
JavaScript je uvek tražen, a ako ste radoznali da učite, savetovao bih vam da proverite ovaj Udemy kurs.