JavaScript · Синтаксис · Просунутий рівень
Цикл подій
Як JavaScript обробляє конкурентність з одним потоком і однією чергою. Покриває стек викликів, чергу задач, чергу мікрозадач і порядок `setTimeout` vs `Promise.resolve`.
Короткий вступ до теми та пояснення перед вправами (вправи нижче):
Приклади порядку, async/await, мікрозадача в макрозадачі, передбачення
#Таблиця довідника, пастки, await в циклі, Web Workers
#Вправи:
Передбачити: синхронний стек викликів
#Не запускаючи, передбачте точний порядок виводу: ```js function a() { console.log('a'); b(); } function b() { console.log('b'); } console.log('start'); a(); console.log('end'); ``` Запишіть очікуваний вивід у коментарях, потім перевірте запуском.
function a() { console.log('a'); b(); }
function b() { console.log('b'); }
console.log('start');
a();
console.log('end');
// Очікуваний вивід (запишіть до запуску):
// ???
Рішення
function a() { console.log('a'); b(); }
function b() { console.log('b'); }
console.log('start');
a();
console.log('end');
// Вивід: start, a, b, end
// Стек: main -> a -> b, потім розгортається: b, a, main
setTimeout(0) виконується після синхронного коду
#Передбачте порядок виводу і поясніть ЧОМУ `'2'` з'являється після `'3'`, хоча затримка 0: ```js console.log('1'); setTimeout(() => console.log('2'), 0); console.log('3'); ```
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// Порядок виводу: ???
// Чому?
Рішення
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// Вивід: 1, 3, 2
//
// setTimeout ставить macrotask у чергу задач.
// Поточний синхронний код (1, потім 3) завершується першим.
// Лише потім цикл подій бере колбек із черги (2).
Promise .then() — це мікрозадача
#Передбачте вивід. Потім порівняйте з прикладом `setTimeout(0)` з попередньої вправи та поясніть різницю: ```js console.log('1'); Promise.resolve().then(() => console.log('2')); console.log('3'); ```
console.log('1');
Promise.resolve().then(() => console.log('2'));
console.log('3');
// Вивід: ???
// Чим відрізняється від setTimeout(fn, 0)?
Рішення
console.log('1');
Promise.resolve().then(() => console.log('2'));
console.log('3');
// Вивід: 1, 3, 2
// Той самий порядок, що й з setTimeout, але механізм різний.
//
// Promise .then() — МІКРОЗАДАЧА.
// Мікрозадачі виконуються після поточного завдання, але ДО наступного macrotask.
// setTimeout: sync -> macrotask
// .then(): sync -> microtask -> (потім macrotask якщо є)
Класичний порядок: sync + Promise + setTimeout
#Передбачте точний вивід (4 рядки). Це канонічний тест на порядок циклу подій: ```js console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D'); ```
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// Вивід (4 рядки): ???
Рішення
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// Вивід: A, D, C, B
//
// Крок 1 - sync: A, macro(B) у чергу, micro(C) у чергу, D
// Крок 2 - вичерпати мікрозадачі: C
// Крок 3 - наступний macrotask: B
Ланцюжок .then() та перемежування з setTimeout
#Передбачте вивід: ```js setTimeout(() => console.log('macro'), 0); Promise.resolve() .then(() => console.log('micro 1')) .then(() => console.log('micro 2')) .then(() => console.log('micro 3')); console.log('sync'); ```
setTimeout(() => console.log('macro'), 0);
Promise.resolve()
.then(() => console.log('micro 1'))
.then(() => console.log('micro 2'))
.then(() => console.log('micro 3'));
console.log('sync');
// Вивід: ???
Рішення
setTimeout(() => console.log('macro'), 0);
Promise.resolve()
.then(() => console.log('micro 1'))
.then(() => console.log('micro 2'))
.then(() => console.log('micro 3'));
console.log('sync');
// Вивід: sync, micro 1, micro 2, micro 3, macro
//
// Кожен .then() у ланцюжку додає мікрозадачу коли попередня вирішується.
// Вся черга мікрозадач вичерпується до того, як 'macro' отримає шанс.
async/await призупиняється на кожному await
#Передбачте вивід: ```js async function main() { console.log('async start'); await Promise.resolve(); console.log('async end'); } console.log('before'); main(); console.log('after'); ```
async function main() {
console.log('async start');
await Promise.resolve();
console.log('async end');
}
console.log('before');
main();
console.log('after');
// Вивід: ???
Рішення
async function main() {
console.log('async start');
await Promise.resolve();
console.log('async end');
}
console.log('before');
main();
console.log('after');
// Вивід: before, async start, after, async end
//
// main() виконується синхронно до першого 'await'.
// 'await' призупиняє main() і планує решту як мікрозадачу.
// Виконання повертається на місце виклику: виконується 'after'.
// Потім черга мікрозадач: виконується 'async end'.
queueMicrotask — явне планування мікрозадачі
#Використайте `queueMicrotask` для планування колбека та передбачте, де він з'явиться відносно синхронного коду та `setTimeout(0)`: ```js setTimeout(() => console.log('timeout'), 0); queueMicrotask(() => console.log('microtask')); console.log('sync'); ```
setTimeout(() => console.log('timeout'), 0);
queueMicrotask(() => console.log('microtask'));
console.log('sync');
// Вивід: ???
Рішення
setTimeout(() => console.log('timeout'), 0);
queueMicrotask(() => console.log('microtask'));
console.log('sync');
// Вивід: sync, microtask, timeout
//
// queueMicrotask() планує мікрозадачу, як і Promise.resolve().then().
// Мікрозадачі виконуються до будь-якого macrotask, тому 'microtask' випереджає 'timeout'.
Блокування циклу подій
#Напишіть функцію `blockFor(ms)`, що синхронно блокує головний потік на `ms` мілісекунд через busy loop. Заплануйте `setTimeout` на 50мс, потім викличте `blockFor(200)`. Спостерігайте, що таймаут спрацьовує пізно — ~200мс, не 50мс — і поясніть чому.
function blockFor(ms) {
// busy loop: заблокуйте потік на ms мілісекунд
}
const t0 = Date.now();
setTimeout(() => {
console.log(`Спрацював через ${Date.now() - t0}мс`); // очікується ~200мс
}, 50);
blockFor(200);
console.log(`Sync завершено за ${Date.now() - t0}мс`);
Рішення
function blockFor(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {}
}
const t0 = Date.now();
setTimeout(() => {
console.log(`Спрацював через ${Date.now() - t0}мс`); // ~200мс
}, 50);
blockFor(200);
console.log(`Sync завершено за ${Date.now() - t0}мс`);
// Поки blockFor() виконується, цикл подій застряг.
// Жодні таймери, мережа, події не можуть спрацювати.
// Таймаут на 50мс може спрацювати лише після звільнення потоку.
Мікрозадача всередині macrotask
#Передбачте вивід. Зверніть увагу на мікрозадачу, яка планується ВСЕРЕДИНІ першого колбека `setTimeout`: ```js setTimeout(() => { console.log('macro 1'); Promise.resolve().then(() => console.log('micro inside')); }, 0); setTimeout(() => console.log('macro 2'), 0); ```
setTimeout(() => {
console.log('macro 1');
Promise.resolve().then(() => console.log('micro inside'));
}, 0);
setTimeout(() => console.log('macro 2'), 0);
// Вивід: ???
Рішення
setTimeout(() => {
console.log('macro 1');
Promise.resolve().then(() => console.log('micro inside'));
}, 0);
setTimeout(() => console.log('macro 2'), 0);
// Вивід: macro 1, micro inside, macro 2
//
// Цикл подій: виконує ОДИН macrotask -> вичерпує ВСІ мікрозадачі -> наступний macrotask.
// Тобто: macro 1 -> .then() ставить мікрозадачу -> micro inside -> macro 2
Повний тест на порядок
#Найскладніший варіант — передбачте всі 8 рядків виводу: ```js console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => { console.log('3'); setTimeout(() => console.log('4'), 0); Promise.resolve().then(() => console.log('5')); }); setTimeout(() => console.log('6'), 0); Promise.resolve().then(() => console.log('7')); console.log('8'); ```
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('5'));
});
setTimeout(() => console.log('6'), 0);
Promise.resolve().then(() => console.log('7'));
console.log('8');
// Вивід (8 рядків): ???
Рішення
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('5'));
});
setTimeout(() => console.log('6'), 0);
Promise.resolve().then(() => console.log('7'));
console.log('8');
// Вивід: 1, 8, 3, 7, 5, 2, 6, 4
//
// Фаза 1 sync: 1, macro(2), micro(перший .then), macro(6), micro(7), 8
// Фаза 2 micro: перший .then -> 3, macro(4), micro(5)
// micro(7) -> 7
// micro(5) -> 5
// Фаза 3 macro: 2, 6, 4 (порядок FIFO)