JavaScript · Синтаксис · Просунутий рівень

Цикл подій

10 завдань

Як JavaScript обробляє конкурентність з одним потоком і однією чергою. Покриває стек викликів, чергу задач, чергу мікрозадач і порядок `setTimeout` vs `Promise.resolve`.

Чому однопоточний, стек викликів, черга задач, черга мікрозадач

#
**Чому JavaScript однопоточний** JavaScript розроблявся для браузера де він торкається DOM. Якби два потоки могли змінювати DOM одночасно — потрібні були б блокування. Однопоточність уникає цього — немає гонок даних, дедлоків, м'ютексів. Конкурентність досягається інакше: *не блокуючи* на I/O. Коли браузер робить мережевий запит — він передає його ОС і *реєструє callback* який буде викликаний коли дані прийдуть. Тим часом JS-потік вільний обробляти UI-події, запускати анімації. **Модель циклу подій** Є один стек викликів. Він виконує один фрейм за раз до завершення. Callbacks та мікрозадачі ставляться в чергу і обробляються між фреймами: ``` Стек викликів Черга мікрозадач Черга задач (Макрозадачі) ────────── ─────────────── ────────────────────── │ fn() │ ←── │ .then() │ ←─ │ setTimeout cb │ │ main() │ │ await │ │ I/O callback │ ────────── │ queueMicro │ │ setInterval cb │ ─────────────── ────────────────────── ``` **Повний цикл — крок за кроком:** 1. Виконати поточну задачу (синхронний код) до завершення 2. Повністю спустошити чергу мікрозадач (нові мікрозадачі додані під час спустошення теж виконуються *до* наступної макрозадачі) 3. Рендеринг (тільки браузер, якщо підійшов час фрейму) 4. Взяти наступну макрозадачу з черги задач 5. Повторити **Макрозадачі** (черга задач): `setTimeout`, `setInterval`, I/O callbacks, UI-події **Мікрозадачі** (черга мікрозадач): Promise `.then`/`.catch`/`.finally`, відновлення після `await`, `queueMicrotask()`, `MutationObserver`

Приклади порядку, async/await, мікрозадача в макрозадачі, передбачення

#
**Приклади порядку виконання** ```js // Патерн: sync → мікрозадачі → макрозадачі console.log('A'); // sync setTimeout(() => console.log('D'), 0); // макрозадача Promise.resolve().then(() => console.log('C')); // мікрозадача console.log('B'); // sync // Вивід: A B C D ``` **async/await під капотом** `async/await` — синтаксичний цукор над Promises. Кожен `await` створює межу мікрозадачі — виконання призупиняється і відновлюється як мікрозадача: ```js async function f() { console.log('A'); // виконується синхронно при виклику f() await Promise.resolve(); console.log('C'); // відновлюється як мікрозадача } f(); console.log('B'); // Вивід: A B C ``` **Мікрозадача всередині макрозадачі** ```js setTimeout(() => { console.log('macro 1'); Promise.resolve().then(() => console.log('micro inside macro 1')); }, 0); setTimeout(() => console.log('macro 2'), 0); // Вивід: macro 1 → micro inside macro 1 → macro 2 ``` **Алгоритм передбачення виводу** ```js console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve() .then(() => console.log(3)) .then(() => console.log(4)); console.log(5); // Вивід: 1 5 3 4 2 // // Крок 1 (sync): логує 1, планує setTimeout, ланцюг .then×2, логує 5 // Крок 2 (мікрозадачі): .then(3) → .then(4) // Крок 3 (макрозадача): setTimeout → 2 ``` **`queueMicrotask(fn)`** ```js queueMicrotask(() => console.log('виконується перед наступною макрозадачею')); ```

Таблиця довідника, пастки, await в циклі, Web Workers

#
**Швидкий довідник** | Що | Черга | Пріоритет | |---|---|---| | Синхронний код | поточна задача | найвищий | | `Promise.then/catch/finally` | мікрозадача | перед наступною макрозадачею | | Відновлення після `await` | мікрозадача | перед наступною макрозадачею | | `queueMicrotask()` | мікрозадача | перед наступною макрозадачею | | `setTimeout(fn, 0)` | макрозадача | після всіх мікрозадач | | `setInterval` | макрозадача (повторювана) | після всіх мікрозадач | | `requestAnimationFrame` | перед рендером | лише браузер, ~16мс | **Пастки** **1. Важка синхронна робота блокує все:** ```js // ПОГАНО — заморожує вкладку items.forEach(item => heavyWork(item)); // КРАЩЕ — поступатися циклу подій між порціями async function processAll(items) { for (let i = 0; i < items.length; i++) { heavyWork(items[i]); if (i % 100 === 0) await new Promise(r => setTimeout(r, 0)); } } ``` **2. Нескінченна рекурсія мікрозадач — голодує макрозадачі:** ```js // Заморожує вкладку: function loop() { Promise.resolve().then(loop); } // Безпечний варіант: function loop() { setTimeout(loop, 0); } ``` **3. `await` в циклі:** ```js // Послідовно — кожен fetch чекає попереднього: for (const url of urls) { const data = await fetch(url); } // Конкурентно — всі fetch стартують одразу: const results = await Promise.all(urls.map(url => fetch(url))); ``` **Web Workers — справжній паралелізм для CPU-роботи** ```js // main.js const worker = new Worker('./worker.js'); worker.postMessage({ data: largeArray }); worker.onmessage = e => console.log('result:', e.data); // worker.js self.onmessage = e => { const result = heavyCPUWork(e.data.data); self.postMessage(result); }; ```
01

Передбачити: синхронний стек викликів

#

Не запускаючи, передбачте точний порядок виводу: ```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
02

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).
03

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 якщо є)
04

Класичний порядок: 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
05

Ланцюжок .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' отримає шанс.
06

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'.
07

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'.
08

Блокування циклу подій

#

Напишіть функцію `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мс може спрацювати лише після звільнення потоку.
09

Мікрозадача всередині 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
10

Повний тест на порядок

#

Найскладніший варіант — передбачте всі 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)