Skip to main content

Черга макрозадач (macrotask queue)

Macrotask queue (черга макрозадач) це черга крупних асинхронних задач, які виконуються по одній за ітерацію Event Loop. Кожна макрозадача це «велика» подія: спрацював таймер, користувач клікнув, прийшла відповідь від мережі. Черга мікрозадач має вищий пріоритет, тому макрозадачі завжди чекають своєї черги.

Теорія

TL;DR

  • Макрозадача це «велике» асинхронне завдання: setTimeout, setInterval, setImmediate (Node.js), обробники подій DOM, мережеві колбеки, MessageChannel.
  • За одну ітерацію Event Loop виконується рівно одна макрозадача.
  • Після кожної макрозадачі рушій виконує всі мікрозадачі, які встигли накопичитися.
  • Порядок незмінний: макрозадача, усі мікрозадачі, наступна макрозадача, усі мікрозадачі.
  • Черга макрозадач працює за принципом FIFO: задачі виконуються в порядку додавання.
  • Пріоритет макрозадач нижчий за мікрозадачі, тому Promise.then() спрацює раніше за setTimeout(..., 0).

Швидкий приклад

javascript
console.log('1'); setTimeout(() => console.log('2 (macrotask)'), 0); Promise.resolve().then(() => console.log('3 (microtask)')); console.log('4');

Результат:

javascript
1 4 3 (microtask) 2 (macrotask)

Чому саме так:

  1. 1 і 4 друкуються синхронно, під час виконання основного скрипта.
  2. .then() додає мікрозадачу, вона виконається одразу після того, як стек спорожніє.
  3. setTimeout додає макрозадачу, вона виконається лише на наступній ітерації Event Loop.

Що потрапляє в чергу макрозадач

Джерела макрозадач у браузері та в Node.js:

  • setTimeout
  • setInterval
  • setImmediate (тільки Node.js)
  • обробники подій DOM: click, load, input і подібні
  • мережеві колбеки: XMLHttpRequest, завершення fetch
  • MessageChannel

Спільна риса всіх цих джерел: задача приходить «ззовні» рушія JavaScript, від таймера, від вводу-виводу або від користувача.

Як працює Event Loop крок за кроком

Спрощена, але робоча модель циклу:

  1. Виконується весь синхронний код (Call Stack).
  2. Коли стек порожній, виконуються мікрозадачі.
  3. Далі рушій бере одну макрозадачу з черги і виконує її.
  4. Після завершення цієї макрозадачі виконуються всі мікрозадачі: колбеки промісів, queueMicrotask, process.nextTick тощо.
  5. Потім береться наступна макрозадача, і після неї знову всі мікрозадачі.
ЕтапЩо робить рушій
1Виконує стек синхронного коду
2Виконує всі мікрозадачі
3Бере одну макрозадачу з черги
4Знову виконує всі мікрозадачі
5Повторює цикл спочатку

Схематично один тік виглядає так:

javascript
// [ Tick 1 ] // Call Stack: synchronous code // v // Microtask Queue: promise callbacks // v // Macrotask Queue: setTimeout, events // v // Event Loop alternates between them

Проста аналогія: Event Loop це ресторан. Макрозадачі це основні страви, їх подають по одній. Мікрозадачі це приправи та гарніри, які додають між подачами страв. Після кожної основної страви кухар спершу роздає всі приправи і лише потім береться за наступне замовлення.

Кілька макрозадач поспіль

javascript
setTimeout(() => console.log('A'), 0); setTimeout(() => console.log('B'), 0); setTimeout(() => console.log('C'), 0); console.log('D');

Результат:

javascript
D A B C

Усі три виклики setTimeout() потрапляють у чергу макрозадач і виконуються по одній, у порядку додавання (FIFO). Синхронний console.log('D') завжди попереду, бо він взагалі не проходить через чергу.

Мікрозадачі всередині макрозадачі

javascript
setTimeout(() => { console.log('1 (timeout)'); }, 0); setTimeout(() => { console.log('2 (timeout)'); Promise.resolve().then(() => console.log('3 (microtask)')); }, 0);

Результат:

javascript
1 (timeout) 2 (timeout) 3 (microtask)

Пояснення: після кожної макрозадачі виконуються всі мікрозадачі, створені під час цієї макрозадачі. Рядок 3 це мікрозадача, породжена другою макрозадачею, тому вона виконається до того, як рушій візьме наступну макрозадачу.

Макрозадачі проти мікрозадач

ХарактеристикаMacrotaskMicrotask
ПрикладиsetTimeout, setInterval, події DOM, fetchPromise.then, queueMicrotask, process.nextTick
Коли виконуютьсяПісля того, як спорожніла черга мікрозадачПісля кожної макрозадачі або після синхронного коду
Кількість за циклОдна за ітерацію Event LoopУсі підряд, доки черга не спорожніє
ПріоритетНизькийВисокий
Тип задач«Зовнішні»: таймери, I/O, події«Внутрішні»: логіка самого JavaScript

Підсумок в одному рядку на пункт:

ТермінОпис
Macrotask queueЧерга крупних подій, які виконує браузер або Node.js
Що в ній зберігаєтьсяТаймери, I/O, обробники подій, мережеві колбеки
Як виконуєтьсяПо одній задачі за ітерацію Event Loop
Після кожноїВиконуються всі мікрозадачі
ПріоритетНижчий, ніж у мікрозадач: Promise спрацює швидше, ніж setTimeout

Типові помилки

  • Думати, що setTimeout(fn, 0) виконає fn негайно. Нуль це мінімальна затримка, а не обіцянка. Колбек стає в чергу макрозадач і чекає, доки завершиться синхронний код і спорожніє черга мікрозадач.
  • Плутати пріоритети. Promise.resolve().then(...) завжди випередить setTimeout(..., 0), навіть якщо проміс створено пізніше в коді.
  • Вважати, що за одну ітерацію виконуються всі макрозадачі. Ні, лише одна. Решта чекають наступних ітерацій, тому між ними браузер встигає перемалювати сторінку.
  • Створювати нескінченний ланцюг мікрозадач. Якщо мікрозадача породжує нову мікрозадачу, черга ніколи не спорожніє і жодна макрозадача не отримає шансу виконатися: сторінка зависає.
  • Розраховувати на точність таймерів. Довга макрозадача блокує цикл, тому setTimeout(fn, 100) може спрацювати значно пізніше за 100 мс.
  • Змішувати setImmediate і setTimeout у Node.js. Вони належать до різних фаз циклу, і порядок їх виконання на верхньому рівні модуля не гарантований.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.