Asynchronní JavaScript pod pokličkou aneb Eventloop v praxi

Co znamená asynchronní JavaScript a jak funguje pod pokličkou? K čemu slouží event loop, API a event queue? Jak zařídit, aby váš kód zbytečně neblokoval prohlížeč, tzv. non-blocking kód?

Co je to JavaScript

Javascript je mnoho věcí (high-leveldynamicweakly typed…). Jedna z definic tvrdí „a single threaded non-blocking asynchronous langugage“. V češtině by to znamenalo něco jako „jednovláknový neblokující asynchronní jazyk“. V tomto článku si projdeme každý aspekt této definice. Nejdřívě si však zopakujeme, co je to synchronní a asynchronní kód.

Synchronní vs. asynchronní kód

U synchronního kódu se čeká na jeho dokončení, než se provede další akce. U asynchronního kódu se na jeho dokončení nečeká a pokračuje se dál ve výpočtu.

Příklad synchronního kódu

const result = sum(4, 5); 
console.log(result);

Čekáme na výsledek první operace (sum), než se provede další operace (console.log).

Příklad asynchronního kódu

console.log(1);
setTimeout(() => console.log(2), 100); // po zavolání setTimeout se hned provede další kód
console.log(3);

Jako první se provede console.log(1). Poté se zavolá funkce setTimeout(…) a ihned se provede další operace console.log(3). Po 100 milisekundách se provede anonymní funkce, kterou jsme předali jako první parametr funkce setTimeout.

Single thread aneb jedno vlákno

Snad každý javascriptový vývojář ví, že JavaScript je „single threaded“ (jednovláknový), ale co to vlastně znamená?

Single thread zpracovává pouze jeden příkaz v daný okamžik a je náchylný k zamrznutí. Javascriptový engine tedy zpracovává kód „postupně za sebou“. Má jeden zásobník (call stack), kam ukládá volané funkce s jejich argumenty (lokální proměnné) a haldu (memory heap), kam ukládá objekty. Jelikož engine používá jen jeden zásobník, tak nezvládne vykonávat více operací zároveň. Zásobník umožňuje dvě operace – vložení funkce na vrchol zásobníku (push) a odebrání funkce z vrcholu zásobníku (pop).

Praktický příklad call stacku

Definujme funkci baz a funkci foo.

function baz() {
    console.log('javascript');
}

function foo() {
    return baz();
}

foo();

Volaná funkce a její argumenty se vloží na vrchol zásobníku a po návratu z funkce se volaná funkce odebere z vrcholu zásobníku.

Na začátku je prázdný zásobník. Zavolá se funkce foo(), která se vloží na vrchol zásobníku. Ta v těle volá funkci baz(), která se vloží na vrchol zásobníku. Funkce baz v těle volá funkci console.log('javascript'), která se vloží na vrchol zásobníku. Do konzole se vypíše ‚javascript‘. Následně se odebere ze zásobníku funkce console.log('javascript')Funkce baz je na konci, jelikož nemá v těle return provede se „implicitně“ a odebere se funkce baz() z vrcholu zásobníku. Poté se odebere z vrcholu zásobníku funkce foo(). Zásobník je prázdný.

Celý tento proces najdete přehledně simulovaný v této ukázce (pozn.: nejprve musíte zavřít modální okno a pak kliknout na save + run).

Asynchronnost

Jak tedy fungují asynchronní operace, když je JavaScript jednovláknový a má jen jeden zásobník? Přece víme, že můžeme provést několik requestů na backend a dělat něco jiného, dokud nám nedojde odpověď ze serveru, kterou pak zpracujeme.

Odpovědí je trio API, Event Queue a Event Loop.

API

Javascriptový engine jako takový opravdu umí zpracovávat jen sekvenčně (popořadě). Asynchronní operace jako setTimeout a AJAXové dotazy zpracovává API (Web nebo Node) podle toho, jestli kód vykonáváme v prohlížečí a nebo na serveru. To, jakým způsobem se tam řeší souběžnost (concurency), už javascriptový engine nezajímá.

Event Queue

Jakmile API dokončilo asynchronní operaci (třeba AJAX request), tak callback, který jsme předali asynchronní funkci jako její argument, se vloží do fronty (event queue) na konec.

Event Loop

Poté přichází na řadu Event Loop. Nejznámější pojem v javascriptovém světě a přitom nejmenší část v celém asynchronním výpočetním cyklu. Event loop nedělá nic jiného, než že si hlídá zásobník (call stack) a frontu (event queue). V případě, že je zásobník prázdný, vezme první callback z fronty (event queue) a vloží ho na vrchol zásobníku (call stacku) ke zpracování.

Jak to celé funguje v praxi

doSomethingSync();
setTimeout(function() {
 console.log('async!');
}, 3000);
doSomethingSync();

Na začátku je prázdný zásobník. Zavolá se funkce doSomethingSync() a vloží se na vrchol zásobníku. Provede se tělo funkce a odebere se z vrcholu zásobníku. Pokračujeme dál. Zavolá se funkce setTimeout(cb, 3000). Vloží se na vrchol zásobníku a jelikož je to funkce, kterou nám poskytuje API (Web, Node), tak nic dalšího neřešíme a tím je pro nás volání funkce setTimeout hotové. Odebereme funkci setTimeout z vrcholu zásobníku. Nakonec se zavolá funkce doSomethingSync() a vloží se na vrchol zásobníku. Provede se tělo funkce a odebere se z vrcholu zásobníku.

Zásobník je prázdný. Nesmíme zapomenout, že se nám někde operace setTimeout ještě provádí v API prostředí. Za cca 3 sekundy se operace dokončí a vloží se callback do fronty (event queue). Event loop ověří, jestli je prázdný zásobník (ano, je), vezme první callback z fronty a vloží ho na vrchol zásobníku ke zpracování.

Celý tento proces je z animovaný v tomto příkladu.

Blocking kód

Určitě se vám nekdy stalo. že jste „zasekali“ prohlížeč. Například když jste nevědomky vytvořili opravdu dlouhý cyklus. Prohlížeč nereagoval, nefungovala tlačítka a nešlo označit ani text. Problém je v tom, že prohlížeč se snaží provést překreslení (re-paint) každých 16 ms, a to jen v případě, kdy je prázdný zásobník (call stack). Jelikož se vám podařilo „odpálit stack“, prohlížeč nemá příležitost provést překreslení, protože je zásobník pořád plný. Kódu, který blokuje po dlouhou dobu zásobník, se říká blocking.

Příklad blocking kódu

function blockingSyncLoop(array) {
    array.forEach(function(item) {
      doSomeTimeConsumingStuff();
    });
}
blockingSyncLoop([1, 2, 3, ..., 99999]);

V případě kdy by bylo pole opravdu velké a funkce doSomeTimeConsumingStuff by prováděla časově náročnou synchronní operaci, tak bychom zablokovali zásobník na dostatečně dlouho dobu a narazili bychom na „performance“ problémy v prohlížeči.

Non-blocking kód

Non-blocking kód „neblokuje zásobník“. Dává prostor prohlížeči pro překreslení.

Příklad non-blocking kódu

function nonBlockingAsyncLoop(array, cb) {
    array.forEach(function(item) {
        setTimeout(function() {
            cb(item);
        }, 0);
    })
}

nonBlockingAsyncLoop([1, 2, 3], function(item) {
    doSomeTimeConsumingStuff();
})

Tělo forEach cyklu je samozřejmě pořád synchronní (blocking). Takže se zásobník pořád „zablokuje“ na určitou dobu v závislosti na velikosti pole. Trik je v tom, že využijeme funkci setTimeout, kterou nám poskytuje API. Výpočet probíhá následovně:

  1. Provede se tělo cyklu forEach (3x se zavolá setTimeout).
  2. Do fronty (event queue) se dostanou všechny 3 anonymní funkce, které jsme předali jako argument funkce setTimeout.
  3. Zásobník se vyprázdní (cyklus forEach skončil).
  4. Provede se překreslení (re-paint).
  5. Event loop vezme první callback z fronty a vloží ho ke zpracování na vrchol zásobníku (call stacku).
  6. Zpracuje se synchronní kód na zásobníku a zásobník se vyprázdní.
  7. Pokračujeme rekurzivně krokem 4 (překreslení), dokud nebude fronta prázdná.

Překreslování má větší prioritu než event loop, proto prohlížeč nejdříve provede překreslení (re-paint) a až poté nastupuje event loop. Jelikož po každé „iteraci“ dáváme šanci prohlížeči, aby se překreslil, než event loop vezme další callback ke zpracování, můžeme tak vyřešit některé blocking operace, které byly „pain in the ass“.

Hezkou ukázku, jak to funguje pod pokličkou, najdete v tomto příkladu. Abyste viděli simulaci překreslení v prohlížeči, musíte si zapnout „Simulate rendering“, který zapnete po kliknutím na ikonu „kladiva“ nahoře vlevo. Upozorňuji, že nástroj občas dělá neplechu.

Pár řádků na konec

Tento článek by vám měl dát aspoň nějakou představu o tom, jak „JavaScript“ vlastně funguje. Pokud by vám některé pojmy nebyly z textu jasné, doporučuji si pustit animace, které najdete v odkazech u jednotlivých příkladů. Jelikož je to poměrně rozsáhlé téma, omluvte prosím případná chybějící fakta. Snažil jsem se sem uvést co nejvíce relevantních informací tak, aby se vešly do rozsahu tohoto článku.

Javascript enthusiast, focusing on server side scripting and clean code

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 14

Přehled komentářů

Doli Nepřesná formulace
Tomáš Dusík Re: Nepřesná formulace
Jarda
Tomáš Dusík Re:
Starý Smrďa Rum await
Tomáš Dusík Re: await
Kolemjdouci
Kolemjdouci Re:
Tomáš Dusík Re:
brkerez diky
Tomáš Dusík Re: diky
Radouch Díky za článek
Tomáš Dusík Re: Díky za článek
Ondřej Novák
Zdroj: https://www.zdrojak.cz/?p=20844