Přejít k navigační liště

Zdroják » JavaScript » Asynchronní JavaScript pod pokličkou aneb Eventloop v praxi

Asynchronní JavaScript pod pokličkou aneb Eventloop v praxi

Články JavaScript

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.

Komentáře

Subscribe
Upozornit na
guest
14 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Doli

funkce setTimeout(…), která je asynchronní, takže se nečeká na její výsledek

To není úplně dobrá formulace. Funkce setTimeout je synchronní a kód čeká na výsledek, konkrétně až vrátí timeoutID. Asynchronně se provede pouze funkce nebo kód, který ji předáme jako parametr.

Jarda

Nefunguje vám odkaz v této větě:

Příklad blokujícího kódu najdete v tomto příkladu. Schválně si zkuste označit text.

Vrací to 404

Starý Smrďa Rum

Jak je to s await. Když zavolám

await NecoAsynchronniho();

bude to v tomto případě blokovat?

Kolemjdouci

Diky za clanek, tohle me vzdycky zajimalo a konec to neni zas takova magie :-)

Kolemjdouci

*nakonec

brkerez

Díky moc za fajn článek, stručně, jasně, s přehlednou grafikou, paráda ;)

Radouch

Občas mám pocit, že zde vycházejí články, jejichž cílem je ukázat, že autor je fakt mistr světa v dané problematice. U tohoto článku mám pocit, že je cílem danou problematiku vysvětlit, což se myslím podařilo.
Díky, více takových!

Ondřej Novák

No ve skutečnosti se o ten synchronní kód stará sám procesor včetně zásobníku. Event loop do zásobníku počas běhu synchronního kódu nekouká, on prostě neběží, jen z druhé strany lze do něj vložit event. Je to jako poštovní schránka do které lze vložit příkaz (vykonání nějaké funkce) a když aktuální js vlákno dokončí co právě dělá (zpracovává jinou funkci) vrátí se ke schránce a vyzvedne si další příkaz. Pokud tam žádný není, tak tam aktivně čeká až tam nějaký padne

Tomuto mechanismu se jinde říká dispatching, funkci která to obstarává dispatcher a operaci vložení do fronty dispatch. Ve Windows od prvních verzi to dělá oblíbená sekvence GetMessage TranslateMessage a DispatchMessage. Do fronty se příkazy vkládají přes PostMessage a SendMessage.

Dispatching v jiných jazycích není, ale dá se ho dodělat. Jen se špatně píši knihovny které s ním počítají. Například já mám též v C++ knihovnu která definuje funkci dispatch() a pak funkci runDispatcher která po vzoru JS dělá onen zmíněný eventloop.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.