
Microtask queue v JavaScriptu
Jak funguje microtask queue, event loop a v jakém pořadí se spouští asynchronní kód v JavaScriptu.
JavaScript je jednoduchý. Až na asynchronní kód. Ten je občas záhadný. Zejména pořadí, v jakém se jednotlivé části asynchronního kódu spouštějí.
Klíčem k pochopení je znalost event loopu a rozdíl mezi task queue (někdy též macro task queue) a microtask queue.
Event loop
JavaScript v prohlížeči běží na hlavním vlákně, kde v daném okamžiku může probíhat jen jedna operace. I když existují Web Workers pro práci na pozadí, hlavní vlákno zůstává jednovláknové.
Event loop je mechanismus, který umožňuje asynchronní chování JavaScriptu na hlavním vlákně. Neustále kontroluje, zda je call stack (zásobník volání) prázdný, a pokud ano, vezme další úlohu z fronty a provede ji.
Důležité je, že existují dva typy front:
- Task queue (macro task queue) – pro běžné asynchronní operace
- Microtask queue – pro prioritní operace, které se mají provést co nejdříve
Microtask queue
Microtask queue je prioritní fronta pro operace, které mají být provedeny hned po dokončení aktuálně běžícího skriptu, ale ještě před tím, než prohlížeč provede další rendering nebo zpracuje další úlohu z task queue.
Co patří do microtask queue?
Následující operace vytváří microtasky:
Promise.then(),Promise.catch(),Promise.finally()queueMicrotask()MutationObservercallbackyasync/await(interně používá Promises)
Co patří do task queue?
Běžné asynchronní operace vytváří tasky (macro tasky):
setTimeout()asetInterval()setImmediate()(Node.js)- I/O operace
- UI rendering
- Uživatelské události (click, scroll, …)
Pořadí vykonávání
Event loop funguje následovně:
- Provede se aktuální synchronní kód (call stack)
- Když je call stack prázdný, zpracují se všechny microtasky z microtask queue
- Prohlížeč může provést rendering
- Zpracuje se jeden task z task queue
- Celý cyklus se opakuje
Klíčové je, že microtasky mají prioritu. Pokud se během zpracování microtasku přidá další microtask, zpracuje se ještě před tím, než se prohlížeč dostane k dalšímu tasku.
Interaktivní visualisace
Následující demo ukazuje, jak event loop zpracovává synchronní kód, microtasky a tasky krok za krokem:
Demo ukazuje typický průběh zpracování kódu s setTimeout, Promise a queueMicrotask. Všimněte si, že microtasky se vždy zpracují před tasky!
Praktický příklad
Následující kód ilustruje pořadí vykonávání:
console.log('1: synchronní start');
setTimeout(() => {
console.log('2: setTimeout (macro task)');
}, 0);
Promise.resolve()
.then(() => {
console.log('3: Promise 1 (microtask)');
})
.then(() => {
console.log('4: Promise 2 (microtask)');
});
queueMicrotask(() => {
console.log('5: queueMicrotask (microtask)');
});
console.log('6: synchronní konec');
Výstup bude:
1: synchronní start
6: synchronní konec
3: Promise 1 (microtask)
5: queueMicrotask (microtask)
4: Promise 2 (microtask)
2: setTimeout (macro task)
Vysvětlení pořadí
-
Nejdříve se provede veškerý synchronní kód (řádky 1 a 6)
-
Pak se zpracují všechny microtasky v pořadí, v jakém byly přidány (řádky 3, 5, 4)
-
Nakonec se zpracuje macro task z
setTimeout(řádek 2)
Async/await a microtasky
Funkce označené jako async vždy vrací Promise. Klíčové slovo await pozastaví vykonávání funkce a pokračování funkce se zařadí jako microtask.
async function asyncFunkce() {
console.log('1: start async funkce');
await Promise.resolve();
console.log('2: po await (microtask)');
}
console.log('3: před voláním');
asyncFunkce();
console.log('4: po volání');
Výstup:
3: před voláním
1: start async funkce
4: po volání
2: po await (microtask)
Kód po await se chová jako callback v .then() – je zařazen do microtask queue.
queueMicrotask()
API queueMicrotask() umožňuje explicitně přidat callback do microtask queue:
queueMicrotask(() => {
console.log('Tento kód se spustí jako microtask');
});
Je to čistší a efektivnější alternativa k Promise.resolve().then(...). Na rozdíl od Promise nevytváří zbytečný Promise objekt – jde přímo k věci.
// Starý způsob - vytváří Promise objekt
Promise.resolve().then(() => {
console.log('Microtask přes Promise');
});
// Nový způsob - přímé zařazení do fronty
queueMicrotask(() => {
console.log('Microtask přímo');
});
Reálné použití queueMicrotask()
Batch aktualisace DOM
Seskupení více DOM operací do jedné, aby se stránka překreslila jen jednou:
let updatesPending = false;
const updates = [];
function scheduleUpdate(element, value) {
updates.push({ element, value });
if (!updatesPending) {
updatesPending = true;
queueMicrotask(() => {
// Provede všechny aktualisace najednou
updates.forEach(({ element, value }) => {
element.textContent = value;
});
updates.length = 0;
updatesPending = false;
});
}
}
// Použití - všechny tři aktualisace se provedou najednou
scheduleUpdate(div1, 'hodnota 1');
scheduleUpdate(div2, 'hodnota 2');
scheduleUpdate(div3, 'hodnota 3');
Zpracování chyb mimo try/catch
Oddělení error handlingu od synchronního kódu:
function asyncOperation(data) {
if (!data) {
queueMicrotask(() => {
throw new Error('Data chybí');
});
return;
}
// Zpracování dat...
}
// Chyba se hodí až v microtasku,
// takže try/catch zde nechytí nic
try {
asyncOperation(null);
} catch (e) {
console.log('Toto se nikdy nespustí');
}
// Místo toho použij:
window.addEventListener('error', (e) => {
console.log('Chyba zachycena:', e.message);
});
Plugin/Hook systém
Umožnění pluginům reagovat na události v dalším microtasku:
class EventSystem {
constructor() {
this.hooks = [];
}
registerHook(fn) {
this.hooks.push(fn);
}
trigger(data) {
// Synchronní zpracování
this.processData(data);
// Hooks se spustí až po dokončení
queueMicrotask(() => {
this.hooks.forEach(hook => hook(data));
});
}
processData(data) {
console.log('Zpracování:', data);
}
}
const events = new EventSystem();
events.registerHook(data => console.log('Hook 1:', data));
events.registerHook(data => console.log('Hook 2:', data));
events.trigger('test');
// Výstup:
// Zpracování: test
// Hook 1: test
// Hook 2: test
Více informací: MDN – queueMicrotask()
MutationObserver
MutationObserver slouží k pozorování změn v DOM stromu. Jeho callbacky se spouštějí jako microtasky:
const observer = new MutationObserver(() => {
console.log('DOM se změnil (microtask)');
});
observer.observe(document.body, {
childList: true,
subtree: true
});
document.body.appendChild(document.createElement('div'));
console.log('Prvek přidán (synchronní)');
Výstup:
Prvek přidán (synchronní)
DOM se změnil (microtask)
Pozor na nekonečnou smyčku
Protože se všechny microtasky zpracovávají před dalším taskem, může dojít k zablokování event loopu:
function pridejMicrotask() {
queueMicrotask(() => {
console.log('Microtask');
pridejMicrotask(); // Přidává další microtask
});
}
pridejMicrotask();
setTimeout(() => {
console.log('Tento kód se nikdy nespustí!');
}, 0);
V tomto případě se setTimeout callback nikdy nespustí, protože fronta microtasků se neustále doplňuje.
Praktické využití
Zajištění konsistentního stavu
Microtasky se hodí, když potřebujete provést dokončovací logiku po synchronním kódu, ale ještě před renderingem:
// Nastavíme více hodnot synchronně
element.dataset.loading = 'true';
element.textContent = '';
// V microtasku zajistíme konsistentní stav
// před tím, než prohlížeč vykreslí změny
queueMicrotask(() => {
const data = cache.get('key');
if (data) {
element.textContent = data;
element.dataset.loading = 'false';
}
});
Debouncing pomocí microtasků
Občas je vhodné seskupit více operací do jedné. Například při sledování změn stavu v reactive frameworku:
class StateManager {
constructor() {
this.state = {};
this.listeners = [];
this.updatePending = false;
this.changes = [];
}
setState(key, value) {
const oldValue = this.state[key];
this.state[key] = value;
// Uložit změnu
this.changes.push({ key, oldValue, newValue: value });
// Naplánovat batch aktualisaci
if (!this.updatePending) {
this.updatePending = true;
queueMicrotask(() => {
this.notifyListeners(this.changes);
this.changes = [];
this.updatePending = false;
});
}
}
notifyListeners(changes) {
console.log('Notifikace o změnách:', changes);
this.listeners.forEach(listener => listener(changes));
}
subscribe(listener) {
this.listeners.push(listener);
}
}
// Použití
const state = new StateManager();
state.subscribe(changes => {
console.log(`Provedeno ${changes.length} změn najednou`);
});
// Všechny tři změny se zpracují v jednom microtasku
state.setState('name', 'Jan');
state.setState('age', 30);
state.setState('city', 'Praha');
console.log('Změny naplánované, ale ještě neprovedené');
// Výstup:
// Změny naplánované, ale ještě neprovedené
// Notifikace o změnách: [
// { key: 'name', oldValue: undefined, newValue: 'Jan' },
// { key: 'age', oldValue: undefined, newValue: 30 },
// { key: 'city', oldValue: undefined, newValue: 'Praha' }
// ]
// Provedeno 3 změn najednou
Rozdíly mezi prostředími
Implementace event loopu se mírně liší mezi prohlížečem a Node.js:
- Prohlížeč – zpracovává rendering mezi tasky
- Node.js – má více fází event loopu (timers, I/O callbacks, idle, poll, check, close callbacks)
V Node.js existuje také process.nextTick(), který má ještě vyšší prioritu než microtasky.
Debugování a visualisace
Pro pochopení pořadí vykonávání lze použít:
- Chrome DevTools – Performance tab zobrazuje tasky a microtasky
- Loupe – visualisační nástroj pro event loop (latentflip.com/loupe)
- Console logy – nejjednodušší způsob sledování pořadí
Závěr
-
Event loop je srdce asynchronního JavaScriptu
-
Microtask queue má prioritu před task queue (macro task queue)
-
Promises,
async/await,queueMicrotask()aMutationObserverpoužívají microtasky -
setTimeout,setIntervala události používají tasky -
Všechny microtasky se zpracují před dalším taskem nebo renderingem
Pochopení microtask queue je klíčové pro psaní správného asynchronního kódu a debugování neočekávaného chování.
Odkazy jinam
- Jake Archibald: Tasks, microtasks, queues and schedules – podrobný článek s interaktivními příklady
- MDN: Using microtasks in JavaScript – oficiální dokumentace
- Philip Roberts: What the heck is the event loop anyway? – skvělá přednáška o event loopu