BÄh JavaScriptu v prohlÞeÄi i v Node.js je založen na smyÄce událostÃ.
PorozumÄt, jak smyÄka událostà funguje, je důležité pro optimalizace a nÄkdy pro správnou architekturu.
V této kapitole nejprve probereme teoretické podrobnosti o tom, jak to vÅ¡e funguje, a pak se podÃváme na praktické aplikace tÄchto znalostÃ.
SmyÄka událostÃ
Koncept smyÄky událostà je velmi jednoduchý. Je to nekoneÄná smyÄka, v nÞ motor JavaScriptu Äeká na úlohy, spouÅ¡tà je a pak spà a Äeká na dalšà úlohy.
Obecný algoritmus motoru:
- Dokud jsou úlohy:
- spouÅ¡tÄj je v poÅadà od nejstarÅ¡Ã.
- Äekej, dokud se neobjevà úloha, a pak pÅejdi k bodu 1.
To je formalizace toho, co vidÃme pÅi prohlÞenà stránky. Motor JavaScriptu vÄtÅ¡inu Äasu nic nedÄlá a spustà se jen tehdy, když se aktivuje skript, handler nebo událost.
PÅÃklady úloh:
- Když se naÄte externà skript
<script src="...">, úlohou je spustit jej. - Když uživatel pohne myÅ¡Ã, úlohou je vyvolat událost
mousemovea spustit handlery. - Když nastal Äas na naplánovaný
setTimeout, úlohou je spustit jeho callback. - â¦a podobnÄ.
Ãlohy se nastavà â motor je zpracuje â a pak Äeká na dalšà úlohy (zatÃmco spà a nespotÅebovává témÄŠžádné zdroje CPU).
Může se stát, že úloha pÅijde v dobÄ, kdy motor zrovna pracuje. Pak se úloha zaÅadà do fronty.
Ãlohy tvoÅà frontu, nazývanou âfronta makroúlohâ (pojem z v8):
NapÅÃklad zatÃmco je motor zaneprázdnÄn výkonem skriptu, uživatel může pohnout myšà a vyvolat mousemove, může nastat Äas na setTimeout a podobnÄ. Tyto úlohy vytvoÅà frontu, jak je zobrazeno na uvedeném obrázku.
Ãlohy z fronty se zpracovávajà principem âkdo dÅÃv pÅijde, ten je dÅÃv obslouženâ. Když motor v prohlÞeÄi dokonÄà script, zpracuje událost mousemove, pak handler setTimeout a tak dále.
ZatÃm je to docela jednoduché, že?
Dva dalšà detaily:
- Dokud motor provádà úlohu, nikdy se neprovádà vykreslovánÃ. Nezáležà na tom, že provádÄnà úlohy bude trvat dlouhou dobu. ZmÄny v DOMu se vykreslà až po dokonÄenà úlohy.
- Jestliže úloha trvá pÅÃliÅ¡ dlouho, prohlÞeÄ nemůže provádÄt jiné úlohy, napÅÃklad zpracovánà uživatelských událostÃ. Po nÄjaké dobÄ tedy zobrazà oznámenà jako âStránka nereagujeâ a navrhne ukonÄit úlohu spolu s celou stránkou. To se stane tehdy, když úloha obsahuje spoustu složitých výpoÄtů nebo když programátorská chyba vede k nekoneÄnému cyklu.
To byla teorie. Nynà se podÃvejme, jak můžeme tyto znalosti využÃt.
PÅÃpad použità 1: rozdÄlenà úloh nároÄných na CPU
ÅeknÄme, že máme úlohu nároÄnou na CPU.
NapÅÃklad zvýrazÅovánà syntaxe (použÃvané k obarvenà pÅÃkladů kódu na této stránce) je na CPU pomÄrnÄ nároÄné. Aby bylo možné kód obarvit, musà se provést analýza, vytvoÅit mnoho barevných elementů, pÅidat je do dokumentu â pro velké množstvà textu to trvá dlouhou dobu.
ZatÃmco motor pracuje na zvýrazÅovánà syntaxe, nemůže provádÄt jiné vÄci vztahujÃcà se k DOMu, zpracovávat uživatelské události a podobnÄ. Může dokonce způsobit, že se prohlÞeÄ âzadrhneâ nebo dokonce na nÄjakou dobu âzatuhneâ, což je nepÅijatelné.
TÄmto problémům se můžeme vyhnout, když velkou úlohu rozdÄlÃme na Äásti. ZvýraznÃme prvnÃch 100 Åádků, pak naplánujeme setTimeout (s nulovou prodlevou) pro dalÅ¡Ãch 100 Åádků, a tak dále.
Abychom tento pÅÃstup pÅedvedli, pro zjednoduÅ¡enà mÃsto zvýrazÅovánà syntaxe použijeme funkci, která bude poÄÃtat od 1 do 1000000000.
Když spustÃte následujÃcà kód, motor na nÄjakou dobu âzatuhneâ. U JS na serverové stranÄ je to zjevnÄ vidÄt, a pokud si ho spustÃte v prohlÞeÄi, pokuste se klikat na jiná tlaÄÃtka na stránce â uvidÃte, že dokud poÄÃtánà neskonÄÃ, žádné jiné události nebudou zpracovány.
let i = 0;
let zaÄátek = Date.now();
function poÄÃtej() {
// provede tÄžkou práci
for (let j = 0; j < 1e9; j++) {
i++;
}
alert("Hotovo za " + (Date.now() - zaÄátek) + ' ms');
}
poÄÃtej();
ProhlÞeÄ může dokonce zobrazit upozornÄnà âskript trvá pÅÃliÅ¡ dlouhoâ.
RozdÄlme tuto práci pomocà vnoÅených volánà setTimeout:
let i = 0;
let zaÄátek = Date.now();
function poÄÃtej() {
// provede Äást tÄžké práce (*)
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Hotovo za " + (Date.now() - zaÄátek) + ' ms');
} else {
setTimeout(poÄÃtej); // naplánujeme dalšà volánà (**)
}
}
poÄÃtej();
Nynà je rozhranà prohlÞeÄe bÄhem procesu âpoÄÃtánÃâ plnÄ funkÄnÃ.
Jedno spuÅ¡tÄnà poÄÃtej odvede Äást práce (*) a pak se znovu naplánuje (**), pokud je to zapotÅebÃ:
- Prvnà bÄh spoÄÃtá:
i=1...1000000. - Druhý bÄh spoÄÃtá:
i=1000001..2000000. - â¦a tak dále.
Pokud se nynà objevà nová vedlejšà úloha (napÅ. událost onclick), zatÃmco je motor zaneprázdnÄn výkonem Äásti 1, zaÅadà se do fronty a pak se spustÃ, až bude Äást 1 dokonÄena, jeÅ¡tÄ pÅed dalšà ÄástÃ. Periodické návraty do smyÄky událostà mezi spuÅ¡tÄnÃmi poÄÃtej poskytnou motoru JavaScriptu dostatek âvzduchu k nadechnutÃâ, aby provádÄl nÄco jiného, aby reagoval na jiné uživatelské akce.
Stojà za zmÃnku, že obÄ varianty â s rozdÄlenÃm práce pomocà setTimeout i bez nÄj â majà srovnatelnou rychlost. V celkové dobÄ výpoÄtu nenà velký rozdÃl.
Abychom je jeÅ¡tÄ pÅiblÞili, vytvoÅme zlepÅ¡enÃ.
PÅesuneme naplánovánà na zaÄátek funkce poÄÃtej():
let i = 0;
let zaÄátek = Date.now();
function poÄÃtej() {
// pÅesuneme naplánovánà na zaÄátek
if (i < 1e9 - 1e6) {
setTimeout(poÄÃtej); // naplánujeme nové volánÃ
}
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Hotovo za " + (Date.now() - zaÄátek) + ' ms');
}
}
poÄÃtej();
Když nynà zahájÃme funkci poÄÃtej() a vidÃme, že budeme potÅebovat volat poÄÃtej() dál, naplánujeme to hned, jeÅ¡tÄ pÅed provedenÃm práce.
Když si to spustÃte, vÅ¡imnete si, že to trvá výraznÄ ménÄ Äasu.
ProÄ?
Důvod je jednoduchý: jak si pamatujete, v prohlÞeÄi je minimálnà prodleva pro mnoho vnoÅených volánà funkce setTimeout 4 ms. I když nastavÃme 0, bude to 4 ms (nebo o trochu vÃc). ÄÃm dÅÃve ji tedy nastavÃme, tÃm rychleji se spustÃ.
Nakonec jsme tedy rozdÄlili úlohu nároÄnou na CPU na Äásti â nynà neblokuje uživatelské rozhranÃ. A jejà celková doba bÄhu nenà pÅÃliÅ¡ delÅ¡Ã.
PÅÃpad použità 2: zobrazovánà průbÄhu
Dalšà výhodou rozdÄlenà tÄžkých úloh v prohlÞeÄových skriptech je, že můžeme zobrazovat indikátor průbÄhu.
Jak jsme uvedli dÅÃve, zmÄny v DOMu se vykreslà teprve po dokonÄenà aktuálnÄ bÄžÃcà úlohy, aÅ¥ trvá jakkoli dlouho.
Na jednu stranu je to skvÄlé, protože naÅ¡e funkce může vytvoÅit množstvà elementů, pÅidávat je do dokumentu jeden po druhém a mÄnit jejich styly â návÅ¡tÄvnÃk nespatÅà žádný nedokonÄený âmezistavâ. To je důležité, ne?
V následujÃcÃm demu se zmÄny promÄnné i nezobrazÃ, dokud funkce neskonÄÃ, takže uvidÃme pouze poslednà hodnotu:
<div id="průbÄh"></div>
<script>
function poÄÃtej() {
for (let i = 0; i < 1e6; i++) {
i++;
průbÄh.innerHTML = i;
}
}
poÄÃtej();
</script>
â¦Ale souÄasnÄ můžeme chtÃt pÅi bÄhu úlohy nÄco zobrazovat, napÅÃklad ukazatel průbÄhu.
Jestliže rozdÄlÃme tÄžkou úlohu na Äásti pomocà setTimeout, budou zmÄny vykresleny mezi nimi.
Tohle vypadá lépe:
<div id="průbÄh"></div>
<script>
let i = 0;
function poÄÃtej() {
// provede Äást tÄžké práce (*)
do {
i++;
průbÄh.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e7) {
setTimeout(poÄÃtej);
}
}
poÄÃtej();
</script>
Nynà bude <div> zobrazovat zvyÅ¡ujÃcà se hodnoty i, tedy nÄco jako ukazatel průbÄhu.
PÅÃpad použità 3: provedenà nÄÄeho po události
V handleru události se můžeme rozhodnout odložit nÄkteré akce až na dobu, kdy událost probublala a byla zpracována na vÅ¡ech úrovnÃch. To můžeme udÄlat zapouzdÅenÃm kódu do setTimeout s nulovou prodlevou.
PÅÃklad jsme již vidÄli v kapitole Vyvolávánà vlastnÃch událostÃ: naÅ¡e vlastnà událost otevÅi-menu je vyvolána v setTimeout, takže nastane až poté, co je událost click plnÄ zpracována.
menu.onclick = function() {
// ...
// vytvoÅÃme vlastnà událost s daty kliknutého prvku menu
let vlastnÃUdálost = new CustomEvent("otevÅi-menu", {
bubbles: true
});
// asynchronnÄ vyvoláme vlastnà událost
setTimeout(() => menu.dispatchEvent(vlastnÃUdálost));
};
Makroúlohy a mikroúlohy
KromÄ makroúloh, popsaných v této kapitole, existujà také mikroúlohy, o nichž jsme se zmÃnili v kapitole Mikroúlohy.
Mikroúlohy pocházejà výhradnÄ z naÅ¡eho kódu. Obvykle je vytváÅejà pÅÃsliby: spuÅ¡tÄnà handleru .then/catch/finally se stane mikroúlohou. Mikroúlohy se použÃvajà i âpod pláštÃkemâ await, což je vlastnÄ jen dalšà způsob zpracovánà pÅÃslibů.
Existuje i speciálnà funkce queueMicrotask(funkce), která zaÅadà spuÅ¡tÄnà funkce do fronty mikroúloh.
Ihned po každé makroúloze motor spustà vÅ¡echny úlohy z fronty mikroúloh jeÅ¡tÄ pÅed spuÅ¡tÄnÃm dalÅ¡Ãch makroúloh, vykreslovánà nebo Äehokoli jiného.
PodÃvejme se na pÅÃklad:
setTimeout(() => alert("timeout"));
Promise.resolve()
.then(() => alert("pÅÃslib"));
alert("kód");
V jakém poÅadà se to bude dÃt?
- Jako prvnà se zobrazÃ
kód, protože to je obvyklé synchronnà volánÃ. - Jako druhý se zobrazÃ
pÅÃslib, protože.thenprojde frontou mikroúloh a spustà se po aktuálnÃm kódu. - Jako poslednà se zobrazÃ
timeout, protože to je makroúloha.
Bohatšà obrázek smyÄky událostà vypadá takto (poÅadà shora dolů, tedy: nejprve skript, pak mikroúlohy, vykreslovánà a tak dále):
VÅ¡echny mikroúlohy se dokonÄà dÅÃve, než se uskuteÄnà zpracovánà dalÅ¡Ãch handlerů událostÃ, vykreslovánà nebo jakákoli jiná makroúloha.
To je důležité, neboÅ¥ nám to zaruÄuje, že aplikaÄnà prostÅedà zůstane mezi mikroúlohami v zásadÄ stejné (nebudou zmÄny v souÅadnicÃch myÅ¡i, nová data ze sÃtÄ atd.).
Jestliže chceme spustit funkci asynchronnÄ (po aktuálnÃm kódu), ale jeÅ¡tÄ pÅed vykreslenÃm zmÄn nebo zpracovánà nových událostÃ, můžeme ji naplánovat volánÃm queueMicrotask.
Následuje pÅÃklad s âukazatelem průbÄhu poÄÃtánÃâ, podobný dÅÃve uvedenému, ale mÃsto setTimeout použijeme queueMicrotask. VidÃte, že k vykreslenà dojde až na samotném konci, stejnÄ jako u synchronnÃho kódu:
<div id="průbÄh"></div>
<script>
let i = 0;
function poÄÃtej() {
// provede Äást tÄžké práce (*)
do {
i++;
průbÄh.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e6) {
queueMicrotask(poÄÃtej);
}
}
poÄÃtej();
</script>
ShrnutÃ
PodrobnÄjšà algoritmus smyÄky událostà (aÄkoli oproti specifikaci stále zjednoduÅ¡ený):
- Vyjmi z fronty a spusÅ¥ nejstaršà úlohu z fronty makroúloh (napÅ. skript).
- Spusť všechny mikroúlohy:
- Dokud nenà fronta mikroúloh prázdná:
- Vyjmi z fronty a spusť nejstaršà mikroúlohu.
- Dokud nenà fronta mikroúloh prázdná:
- Vykresli zmÄny, pokud k nÄjakým doÅ¡lo.
- Pokud je fronta makroúloh prázdná, vyÄkej na pÅÃchod nové makroúlohy.
- PÅejdi k bodu 1.
Chcete-li naplánovat novou makroúlohu:
- Použijte
setTimeout(f)s nulovou prodlevou.
TÃmto způsobem můžete rozdÄlit velkou, výpoÄetnÄ nároÄnou úlohu na Äásti, aby mezi nimi prohlÞeÄ mohl reagovat na uživatelské události a zobrazovat průbÄh.
PoužÃvá se to i v handlerech událostÃ, abychom naplánovali akci až na dobu, kdy bude událost plnÄ zpracována (bublánà je dokonÄeno).
Chcete-li naplánovat novou mikroúlohu:
- Použijte
queueMicrotask(f). - Frontou mikroúloh procházejà také handlery pÅÃslibů.
Mezi mikroúlohami nedocházà ke zpracovánà událostà uživatelského rozhranà nebo sÃtÄ: spouÅ¡tÄjà se okamžitÄ jedna za druhou.
Můžeme tedy chtÃt použÃt queueMicrotask, abychom spustili funkci asynchronnÄ, ale ve stejném stavu prostÅedÃ.
Pro dlouhé a tÄžké výpoÄty, které by nemÄly blokovat smyÄku událostÃ, můžeme použÃt Web Workers.
To je způsob, jak spustit kód v jiném, paralelnÃm vláknÄ.
Web Workers si mohou vymÄÅovat zprávy s hlavnÃm procesem, ale majà své vlastnà promÄnné a svou vlastnà smyÄku událostÃ.
Web Workers nemajà pÅÃstup k DOMu, jsou tedy užiteÄné zejména k výpoÄtům, abychom využili vÃce jader CPU souÄasnÄ.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)