Черга макрозадач (macrotask queue)
Macrotask queue (черга макрозадач) це черга крупних асинхронних задач, які виконуються по одній за ітерацію Event Loop. Кожна макрозадача це «велика» подія: спрацював таймер, користувач клікнув, прийшла відповідь від мережі. Черга мікрозадач має вищий пріоритет, тому макрозадачі завжди чекають своєї черги.
Теорія
TL;DR
- Макрозадача це «велике» асинхронне завдання:
setTimeout,setInterval,setImmediate(Node.js), обробники подій DOM, мережеві колбеки,MessageChannel. - За одну ітерацію Event Loop виконується рівно одна макрозадача.
- Після кожної макрозадачі рушій виконує всі мікрозадачі, які встигли накопичитися.
- Порядок незмінний: макрозадача, усі мікрозадачі, наступна макрозадача, усі мікрозадачі.
- Черга макрозадач працює за принципом FIFO: задачі виконуються в порядку додавання.
- Пріоритет макрозадач нижчий за мікрозадачі, тому
Promise.then()спрацює раніше заsetTimeout(..., 0).
Швидкий приклад
console.log('1');
setTimeout(() => console.log('2 (macrotask)'), 0);
Promise.resolve().then(() => console.log('3 (microtask)'));
console.log('4');Результат:
1
4
3 (microtask)
2 (macrotask)Чому саме так:
1і4друкуються синхронно, під час виконання основного скрипта..then()додає мікрозадачу, вона виконається одразу після того, як стек спорожніє.setTimeoutдодає макрозадачу, вона виконається лише на наступній ітерації Event Loop.
Що потрапляє в чергу макрозадач
Джерела макрозадач у браузері та в Node.js:
setTimeoutsetIntervalsetImmediate(тільки Node.js)- обробники подій DOM:
click,load,inputі подібні - мережеві колбеки:
XMLHttpRequest, завершенняfetch MessageChannel
Спільна риса всіх цих джерел: задача приходить «ззовні» рушія JavaScript, від таймера, від вводу-виводу або від користувача.
Як працює Event Loop крок за кроком
Спрощена, але робоча модель циклу:
- Виконується весь синхронний код (Call Stack).
- Коли стек порожній, виконуються мікрозадачі.
- Далі рушій бере одну макрозадачу з черги і виконує її.
- Після завершення цієї макрозадачі виконуються всі мікрозадачі: колбеки промісів,
queueMicrotask,process.nextTickтощо. - Потім береться наступна макрозадача, і після неї знову всі мікрозадачі.
| Етап | Що робить рушій |
|---|---|
| 1 | Виконує стек синхронного коду |
| 2 | Виконує всі мікрозадачі |
| 3 | Бере одну макрозадачу з черги |
| 4 | Знову виконує всі мікрозадачі |
| 5 | Повторює цикл спочатку |
Схематично один тік виглядає так:
// [ Tick 1 ]
// Call Stack: synchronous code
// v
// Microtask Queue: promise callbacks
// v
// Macrotask Queue: setTimeout, events
// v
// Event Loop alternates between themПроста аналогія: Event Loop це ресторан. Макрозадачі це основні страви, їх подають по одній. Мікрозадачі це приправи та гарніри, які додають між подачами страв. Після кожної основної страви кухар спершу роздає всі приправи і лише потім береться за наступне замовлення.
Кілька макрозадач поспіль
setTimeout(() => console.log('A'), 0);
setTimeout(() => console.log('B'), 0);
setTimeout(() => console.log('C'), 0);
console.log('D');Результат:
D
A
B
CУсі три виклики setTimeout() потрапляють у чергу макрозадач і виконуються по одній, у порядку додавання (FIFO). Синхронний console.log('D') завжди попереду, бо він взагалі не проходить через чергу.
Мікрозадачі всередині макрозадачі
setTimeout(() => {
console.log('1 (timeout)');
}, 0);
setTimeout(() => {
console.log('2 (timeout)');
Promise.resolve().then(() => console.log('3 (microtask)'));
}, 0);Результат:
1 (timeout)
2 (timeout)
3 (microtask)Пояснення: після кожної макрозадачі виконуються всі мікрозадачі, створені під час цієї макрозадачі. Рядок 3 це мікрозадача, породжена другою макрозадачею, тому вона виконається до того, як рушій візьме наступну макрозадачу.
Макрозадачі проти мікрозадач
| Характеристика | Macrotask | Microtask |
|---|---|---|
| Приклади | setTimeout, setInterval, події DOM, fetch | Promise.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. Вони належать до різних фаз циклу, і порядок їх виконання на верхньому рівні модуля не гарантований.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.