Jak funguje Event Loop v JavaScriptu?

Photo of author

By etechblogcz

Ačkoli psaní plnohodnotného produkčního kódu může vyžadovat hluboké znalosti jazyků jako C++ a C, JavaScript často zvládnete i se základním porozuměním jeho možnostem.

Koncepty jako předávání funkcí jako parametrů (callback) nebo vytváření asynchronního kódu nejsou obvykle složité na implementaci. Mnoho JavaScriptových vývojářů se proto příliš nezajímá o to, co se odehrává pod povrchem. Nezkoumají složitosti, které jazyk elegantně skrývá.

Nicméně, pro vývojáře JavaScriptu je klíčové porozumět mechanismům, které se skrývají pod kapotou. Znalost toho, jak tyto abstrakce fungují, nám pomáhá dělat informovanější rozhodnutí a tím i znatelně zlepšit výkon našeho kódu.

Tento článek se zaměří na jeden z klíčových, avšak často ne zcela pochopených konceptů v JavaScriptu: **Smyčku událostí (Event Loop)**.

Psaní asynchronního kódu je v JavaScriptu nevyhnutelné. Ale co přesně znamená „asynchronně běžící kód“? Odpovědí je: právě smyčka událostí.

Než se ponoříme do fungování smyčky událostí, musíme si nejprve objasnit, co JavaScript vlastně je a jak pracuje!

Co je JavaScript?

Než budeme pokračovat, vraťme se k naprostým základům. Co vlastně JavaScript je? Mohli bychom jej definovat jako:

JavaScript je vysokoúrovňový, interpretovaný, jednovláknový, neblokující, asynchronní a souběžný jazyk.

Chvíli počkat, cože? To zní jako poučka z učebnice, že? 🤔

Pojďme si to rozebrat!

Klíčovými pojmy v této definici jsou: jednovláknový, neblokující, souběžný a asynchronní.

Jedno vlákno

Vlákno je nejmenší sekvence programových instrukcí, kterou může plánovač řídit nezávisle. Jednovláknový programovací jazyk znamená, že může provádět pouze jeden úkol nebo operaci v daném okamžiku. To znamená, že proces provede od začátku do konce, aniž by bylo vlákno přerušeno nebo pozastaveno.

To se liší od vícevláknových jazyků, kde může být spuštěno více procesů na několika vláknech najednou bez vzájemného blokování.

Jak je ale možné, že JavaScript je jednovláknový a zároveň neblokující?

A co to vlastně znamená „blokování“?

Neblokování

Neexistuje žádná formální definice blokování. V podstatě to znamená, že některé operace na vlákně probíhají pomalu. Neblokování tedy znamená, že operace na vlákně nejsou pomalé.

Ale počkat, tvrdil jsem, že JavaScript běží v jednom vlákně. A také, že je neblokující, tedy že úkoly na zásobníku volání běží rychle. Jak je to možné? A co časovače, smyčky?

Vydržte, brzy to všechno objasníme 😉.

Souběžné

Souběžnost znamená, že kód je spouštěn více vlákny současně.

Dobře, situace se stává poněkud zamotaná. Jak může být JavaScript jednovláknový a zároveň souběžný? Tedy jak může provádět kód s více než jedním vláknem?

Asynchronní

Asynchronní programování znamená, že kód běží v rámci smyčky událostí. Když dojde k operaci, která by mohla blokovat vlákno, je spuštěna událost. Blokující kód se tak provádí, aniž by blokoval hlavní vlákno. Po dokončení blokující operace se výsledek zařadí do fronty a je odeslán zpět do zásobníku.

Ale JavaScript má jen jedno vlákno? Kdo tedy provádí blokující kód a zároveň umožňuje provádění dalších kódů ve vlákně?

Než budeme pokračovat, udělejme si krátké shrnutí toho, co jsme si řekli:

  • JavaScript je jednovláknový.
  • JavaScript je neblokující, tj. pomalé procesy neblokují jeho spouštění.
  • JavaScript je souběžný, tj. provádí svůj kód současně na více vláknech.
  • JavaScript je asynchronní, tj. spouští blokující kód na jiném místě.

Ale výše uvedené definice se tak úplně neshodují. Jak může být jednovláknový jazyk zároveň neblokující, souběžný a asynchronní?

Pojďme se podívat hlouběji na běhové prostředí JavaScriptu, V8. Možná má skrytá vlákna, o kterých nevíme.

Engine V8

Engine V8 je open-source, vysoce výkonné runtime prostředí pro WebAssembly a JavaScript, napsané v C++ společností Google. Většina prohlížečů používá pro běh JavaScriptu právě V8. Používá jej i oblíbené prostředí Node.js.

Jednoduše řečeno, V8 je program v C++, který přijme kód v JavaScriptu, zkompiluje jej a spustí.

V8 provádí dvě hlavní činnosti:

  • Alokace paměti na haldě.
  • Kontext provádění na zásobníku volání.

Naše podezření bohužel nebylo správné. V8 má skutečně jen jeden zásobník volání. Představte si zásobník volání jako vlákno.

Jedno vlákno === jeden zásobník volání === jedno spuštění v daném okamžiku.

Obrázek – Hacker Noon

Pokud má V8 jen jeden zásobník volání, jak může JavaScript běžet souběžně a asynchronně bez blokování hlavního vlákna?

Zkusme to zjistit analýzou jednoduchého, avšak typického asynchronního kódu.

JavaScript spouští každý řádek kódu postupně, jeden po druhém (jednovláknově). Jak se dalo očekávat, první řádek se vypíše do konzole, ale proč se poslední řádek vypíše před kódem časového limitu? Proč proces provádění nečeká na kód časového limitu (blokování), než spustí poslední řádek?

Zdá se, že nám s tímto časovým limitem pomohlo nějaké jiné vlákno, protože jsme si jisti, že vlákno může v daném okamžiku provést pouze jeden úkol.

Pojďme se nenápadně podívat na zdrojový kód V8.

Počkat, cože? Ve V8 nejsou žádné funkce časovače, žádný DOM? Žádné události? Žádný AJAX?… Jééééé!!!

Události, DOM, časovače atd. nejsou součástí základní implementace JavaScriptu. JavaScript se řídí specifikacemi Ecma Script a jeho různé verze jsou často označovány dle specifikací Ecma Script (ES X).

Pracovní postup provádění

Události, časovače a AJAX požadavky jsou poskytovány na straně klienta prostřednictvím prohlížečů a často se nazývají webová rozhraní API. To jsou mechanismy, které umožňují jednovláknovému JavaScriptu být neblokující, souběžný a asynchronní. Ale jak?

Existují tři hlavní komponenty procesu provádění libovolného JavaScriptového programu: zásobník volání, webová rozhraní API a fronta úloh.

Zásobník volání

Zásobník je datová struktura, ve které je poslední přidaný prvek první odstraněn (LIFO – Last In First Out). Představte si to jako hromadu talířů, ze které můžete vždy odebrat pouze ten vrchní. Zásobník volání je datová struktura, která se používá k provádění úkolů a kódu.

Podívejme se na příklad:

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

Při zavolání funkce printSquare() je tato vložena do zásobníku volání. Funkce printSquare() volá funkci square(). Funkce square() je vložena do zásobníku a volá funkci multiply(). Funkce multiply() je vložena do zásobníku. Protože funkce multiply() vrací hodnotu a je poslední funkcí vloženou do zásobníku, je jako první zpracována a odstraněna ze zásobníku, následuje funkce square() a poté funkce printSquare().

Webová rozhraní API

Zde se spouští kód, který engine V8 nezpracovává, aby se „neblokovalo“ hlavní vlákno. Když zásobník volání narazí na funkci webového API, proces se okamžitě předá webovému API, kde je spuštěn. Tím se uvolní zásobník volání pro provádění dalších operací.

Vraťme se k našemu příkladu s funkcí setTimeout():

Když spustíme kód, první řádek s console.log se přesune do zásobníku a výstup se nám téměř okamžitě vypíše. Po dosažení časového limitu jsou časovače zpracovány prohlížečem, protože nejsou součástí základní implementace V8. Místo toho se přesunou do webového rozhraní API a uvolní se tak zásobník pro provádění dalších operací.

Zatímco časový limit stále běží, zásobník pokračuje k dalším akcím a spustí poslední console.log, což vysvětluje, proč se nám vypisuje před výstupem časovače. Jakmile je časovač dokončen, něco se stane. console.log z časovače se jakoby zázrakem objeví znovu v zásobníku volání!

Jak?

Smyčka událostí

Než probereme smyčku událostí, projdeme si funkci fronty úloh.

Vraťme se k našemu příkladu s časovým limitem. Jakmile webové rozhraní API dokončí vykonávání úkolu, automaticky ho nevrátí do zásobníku volání. Nejdříve ho přesune do fronty úloh.

Fronta je datová struktura, která pracuje na principu „First In First Out“, takže když se úlohy zařadí do fronty, jsou i ve stejném pořadí vybrány. Úkoly, které byly provedeny pomocí webového rozhraní API a jsou odeslány do fronty úloh, se poté vrátí do zásobníku volání, aby se mohl vypsat jejich výsledek.

Ale počkat. CO JE TO SAKRA TA SMYČKA UDÁLOSTÍ?

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

Smyčka událostí je proces, který čeká, až se zásobník volání uvolní, a poté přenese zpětná volání (callback funkce) z fronty úloh do zásobníku volání. Jakmile je zásobník prázdný, smyčka událostí se aktivuje a zkontroluje frontu úloh. Pokud nějaká zpětná volání existují, vloží je do zásobníku volání. Poté čeká, až se zásobník volání znovu uvolní a opakuje tento proces.

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

Výše uvedený diagram znázorňuje základní pracovní postup mezi smyčkou událostí a frontou úloh.

Závěr

Ačkoli se jedná o velice stručný úvod, koncept asynchronního programování v JavaScriptu nám dává základní přehled o tom, co se děje pod kapotou. Ukazuje nám, jak je JavaScript schopen běžet souběžně a asynchronně s jediným vláknem.

JavaScript je vždy žádaný, a pokud se ho chcete naučit, doporučuji vám podívat se na tento kurz na Udemy.