webdevlpr

JavaScript se izvrsava u jednoj niti (single-threaded) 🧵, sto znaci da sve operacije koje obavlja JavaScript jesu sinhrone i blokirajuce. Petlja dogadjaja (event loop) je razlog zasto JavaScript djeluje kao asinhroni neblokirajuci jezik. Sve ovo odrzava nekoliko pametnih struktura podataka iza sebe, ali hajde da se vratimo na pocetak.

Sinhrono vs asinhrono izvrsavanje

JavaScript je po svojoj prirodi sinhroni programski jezik, sto znaci da se operacije izvrsavaju jedna za drugom u nizu, na nacin da se jedna operacija mora zavrsiti prije nego se predje na sledecu.

To moze biti problem kada se radi o operacijama koje traze vise vremena, kao sto su zahtijevanje podataka preko mreze, citanje/zapisivanje velikih fajlova itd. U takvim slucajevima, korisnicki interfejs je blokiran dok se operacija ne zavrsi, sto vodi ka losem korisnickom iskustvu i 😠. Mozemo zaboraviti na interaktivnost sa stranicom dok se ova velika operacija ne zavrsi.

Ali asinhrono izvrsavanje instrukcija je jedan od znacajnih razloga zasto je
JavaScript tu gdje jeste. Drugim rijecima, umjesto da cekamo da jedna operacija zavrsi prije nego pocne druga, instrukcije se mogu nastaviti izvrsavati paralelno.

UKUPNO VRIJEMEZADACIASINHRONOSINHRONO

Ovo je moguce jer okruzenje poput Node.js i pretrazivaca imaju event loop. Event loop zahtjevnije/sporije operacije moze predati pretrazivacu. Kada pretrazivac obradi operaciju, rezultat vraca u glavnu skriptu (npr. u obliku Promise objekta) u red zadataka i ceka da JavaScript engine postane slobodan kako bi izvrsio ostatak koda.

V8

V8 engin je odgovoran za brzo izvrsavanje JavaScript koda. Napisan je u C++ sto utice na dobar performas. V8 kompajlira kod u masinski i izvrsava u procesoru racunara. Izmedju ostalog ga koriste Chrome, Opera, Microsoft Ege, Brave (Chromium pretrazivaci) i Node.js.

Call stack

Pozivni stek (call stack) je Last In First out (LIFO) struktura podataka koja prati pozive funkcija u programu. Kada se funkcija pozove, dodaje se na vrh pozivnog steka i sklanja tek kada se izvrsi. Ovo omogucava V8 enginu da prati red izvrsavanja programa.

Ako razmislite o tome, ovu strukturu vjerovatno koristite kada jedete palacinke, osim ako je smjelo od mene da pretpostavim da cekate da sve budu gotove prije nego ih pojedete.

Okej je ako objasnjenje iznad nije dovoljno da potpuno procesuirate call stack i LIFO strukturu podataka, ali kod i praktican primjer govore hiljadu rijeci (i debagovanje isto 😶). Zato je sledece pitanje sta je rezultat koda ispod i kako se on izvrsava?

function weAreAtTheTop() {
console.log('weAreAtTheTop is executed');
}

function forward() {
weAreAtTheTop();
console.log('forward is executed');
}

let move = function() {
forward();
console.log('move is executed');
};

move();

console.log('next instruction');

Rezultat u konzoli je:

weAreAtTheTop is executed
forward is executed
move is executed
next instruction

Evo kratke vizualizacije izvrsavanja koda i prikaza u pozivnom steku (Last In First Out struktura podataka). Prvo se poziva move() funkcija na liniji 15.

Prikaz izvrsavanja pozivnog steka u JavaScriptu
Kod vizualiziramo uz pomoc alata koji se moze pronaci ovdje

Ovako to radimo jer single-threaded znaci da imamo jedan call stack na kom se obradjuje jedna naredba istovremeno: sinhrono izvrsavanje.

Evo primjera kako to moze uticati na korisnicko iskustvo: zamislite da kliknete na dugme koje otvara pop-up prozor, ali V8 engine je vec zauzet izvrsavanjem neke druge CPU gladne naredbe. U tom slucaju, na ekranu se nista nece dogoditi, cak ni ako ponovo kliknete na dugme. Ukoliko postoji animirani GIF na stranici, on je zaustavljen i djeluje kao staticna slika. Kada se V8 engine oslobodi i obradi klikove, pretrazivac ce prikazati niz pop-up prozora i nastaviti s animacijom GIF-a.

Task queue i web API

Scenario iznad implicira da je pretrazivac bio svjestan klikova i da su oni negdje zabiljezeni iako nisu izvrseni istog trenutka. A to negdje je red zadataka (task queue) koji ima First In First Out (FIFO) strukturu podataka, kao onaj u prodavnici na kasi. Da bi ovi zadaci iz task queue dosli na red u pozivni stek, moraju sacekati da se instrukcije iz glavne skripte sinhrono izvrse i da call stack postane slobodan. Tada se mogu izvrsavati task / callback queue zadaci.

Kako uopste operacija dospije u task queue? Ovdje u sliku dolaze WebAPI-jevi.

Web API (Application Programming Interface) je skup alata i metoda koje su dostupne u pretrazivacu. Oni prosiruju mogucnosti JavaScript-a. JavaScript u sebi nema ugradjen pristup DOM-u (Document Object Model), niti mogucnost da slusa klik na dugme, salje mrezne zahtjeve ili odlozi dogadjaje uz setTimeout() metodu.

Ovo su karakteristike okruzenja u kom se JavaScript izvrsava. Kao takve ih obradjuje pretrazivac paralelno sa call stackom koji se bavi sinhronim operacijama koje JavaScript engine izvrsava sam bez pomoci petlje dogadjaja.

Call stack je inicijalno samo registrovao slusanje dogadjaja. Dogadjaj je asinhrona operacija koja nije ugradjena u JavaScript, vec u pretrazivac. Nakon registracije, pretrazivac ce slusati i cekati klik. Kada se klik dogodi, on ide u "red cekanja" koji sada poznajemo kao task queue. Event loop ga iz task queue prebacuje u call stack kada primjeti da je call stack slobodan. Tek tada se klik obradjuje.

Event loop

Koncept petlje dogadjaja je jednostavan. Postoji beskonacna petlja u kojoj JavaScript izvrsava zadatke od najstarijeg (First in First Out), ceka na nove i do tada spava. JavaScript engine vecinu vremena miruje.

Medju prvim zadacima je izvrsavanje eskterne skripte <script src="..."> koja obicno bude postavljena u html dokument i ona kreira GCE (Global Execution Context), pored toga mozda otprema klik i izvrsava hadnlere (funkcije koje "hendluju" odgovor na dogadjaj).

U JavaScript-u globalni kontekst izvrsavanja je defaultni kontekst izvrsavanja i kreira se kada JavaScript engine pocne pokretati nas kod. Kontekst izvrsavanja funkcije kreira se kad god se pozove funkcija i predstavlja lokalni opseg funkcije.

Takodje, treba znati da postoji dvije vrste zadataka: mikro i makro.

Makrozadaci

Makrozadaci su zadaci koji zahtijevaju vise vremena za izvrsenje i dodaju se na kraj reda. Primjeri makrozadataka ukljucuju setTimeout, setInterval, DOM manipulaciju i mrezne zahtjeve. Kada se makrozadatak doda u red, event loop provjerava postoje li drugi zadaci koji se trenutno izvrsavaju i stavlja makrotask u call stack ako JavaScript engine miruje.

Mikrozadaci

Mikrozadaci su zadaci koji zahtijevaju manje vremena za izvrsenje i izvrsavaju se odmah nakon sto se zadatak koji se trenutno izvrsava izvrsi. Mikrozadaci dolaze iskljucivo iz naseg koda. Obicno ih kreiraju Promise, sa .then/catch/finally/await hendlerima. Postoji i posebna funkcija queueMicrotask(func) koja dodaje callback funkciju u red za izvrsavanje mikrozadataka.

Red izvrsavanja

Odmah po izvrsavanju jednog makrozadatka, JavaScript engine izvrsava sve mikrozadatke u redu prije nego nastavi izvrsavati preostale makrozadatke. Evo primjera koda koji to ilustruje:

console.log('start');

// makrozadatak
setTimeout(() => {
console.log('timeout');
}, 0);

// mikrozadatak
Promise.resolve().then(() => {
console.log('promise');
// drugi mikrozadatak
Promise.resolve().then(() => console.log('nested promise'));
});

console.log('end');

// kozola:
start
end
promise
nested promise
timeout

Gdje se tu uklapa renderovanje? Izmedju izvrsenja svih mikrozadataka i pokretanja novog makro zadatka.

makrozadatakmikrozadacirenderovanje

Kada je JavaScript engine zauzet renderovanje nece biti izvrseno, bez obzira na to koliko ce zadatak uzeti vremena. Ako zadatak traje predugo, pretrazivac moze prestati reagovati i predloziti da potpuno "ubijete zadatak" zajedno sa stranicom.

Ukratko

  1. Izvrsi najstariji makrozadatak iz reda
  2. Izvrsi sve mikrozadatke, pocevsi od najstarijeg
  3. Renderuj promjene ako ih ima
  4. Ako nema zadataka, cekaj

Dodatni izvori