Мікрозадача проміса
Мікрозадача (microtask) це невелика задача, яка виконується після поточного стека викликів, але до того, як рушій перейде до макрозадач (setTimeout, setInterval, події DOM). Обробники проміса завжди потрапляють саме в чергу мікрозадач, тому й виконуються раніше за таймери.
Теорія
TL;DR
- Мікрозадачі це «черга термінових задач», яка спустошується одразу після поточного синхронного коду.
.then(),.catch(),.finally()не викликаються синхронно: вони ставляться в чергу мікрозадач.- Після кожної макрозадачі рушій виконує всі мікрозадачі, і лише потім бере наступну макрозадачу.
- Мікрозадачі виконуються в порядку додавання, а нова мікрозадача, створена під час обробки черги, виконається в тому ж проході.
- Тому колбек проміса завжди випереджає
setTimeout(..., 0).
Швидкий приклад
console.log('1');
Promise.resolve().then(() => console.log('2'));
console.log('3');Результат:
1
3
2Чому саме так:
- Спершу виконується весь синхронний код (у стеку викликів).
- Потім Event Loop бачить, що стек порожній, і виконує всі мікрозадачі, у цьому випадку колбек
.then().
Проміси створюють мікрозадачі
Коли проміс переходить у стан fulfilled або rejected, його обробники .then(), .catch(), .finally() не виконуються одразу, а ставляться в чергу мікрозадач (Microtask Queue).
Мікрозадачі створюються:
- при
.then(),.catch(),.finally()уPromise; - при
queueMicrotask(fn); - при
MutationObserver(у браузері); - при
process.nextTick()у Node.js.
Кожен await усередині async-функції теж планує мікрозадачу: продовження функції після await це та сама мікрозадача проміса.
Черга мікрозадач і черга макрозадач
Рушій JavaScript обробляє події так:
- Виконує весь синхронний код (Call Stack).
- Виконує всі мікрозадачі, доки черга не спорожніє.
- Переходить до однієї макрозадачі (наприклад,
setTimeout). - Знову виконує всі мікрозадачі, що накопичилися після неї.
- І так по колу.
Порівняння з макрозадачами:
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');Вивід:
A
D
C
BЧому:
A,Dсинхронно;Cмікрозадача (від проміса);Bмакрозадача (відsetTimeout).
Мікрозадача C виконується раніше за таймер B.
Важливе правило: після виконання кожної макрозадачі рушій завжди повністю спустошує чергу мікрозадач, перш ніж узяти наступну макрозадачу.
Проста аналогія: Event Loop це черга в кафе. Макрозадачі це звичайні великі замовлення, мікрозадачі це швидка решта та розрахунок. Бариста робить замовлення (синхронний код), потім роздає всю решту (мікрозадачі) і лише після цього переходить до наступного клієнта (нової макрозадачі).
Порядок усередині черги
Кілька мікрозадач виконуються строго в порядку додавання:
Promise.resolve().then(() => console.log('1'));
Promise.resolve().then(() => console.log('2'));
Promise.resolve().then(() => console.log('3'));
console.log('4');Вивід:
4
1
2
3Усі .then() потрапляють в одну чергу мікрозадач і виконуються в порядку додавання, після завершення синхронного коду (4).
Мікрозадача, створена всередині іншої мікрозадачі, теж виконається в цьому ж проході:
Promise.resolve().then(() => {
console.log('A');
Promise.resolve().then(() => console.log('B'));
});Вивід:
A
BПісля A виконується нова мікрозадача B, додана під час виконання попередньої. Рушій не виходить із черги, поки вона не спорожніє повністю.
Підсумок
| Тип задачі | Приклади | Коли виконується |
|---|---|---|
| Мікрозадачі | .then(), .catch(), .finally(), queueMicrotask() | Після поточного коду, до наступної макрозадачі |
| Макрозадачі | setTimeout, setInterval, fetch, події DOM | Після всіх мікрозадач поточного тіку |
Проміси завжди використовують мікрозадачі, тому їхні колбеки виконуються раніше за таймери.
Типові помилки
- Вважати, що
.then()виконується синхронно. Навіть у вже виконаногоPromise.resolve()колбек піде в чергу і спрацює після решти синхронного коду. - Розраховувати, що
setTimeout(..., 0)спрацює «негайно». Це макрозадача, вона чекає, поки черга мікрозадач спорожніє, тому будь-який.then()її випередить. - Нескінченно підживлювати чергу мікрозадач. Якщо мікрозадача щоразу додає нову, черга не спорожніє ніколи: рендеринг і таймери просто не отримають шансу виконатися, і сторінка «замерзне».
- Плутати
process.nextTick()і мікрозадачі промісів у Node.js.nextTickмає власну чергу з вищим пріоритетом, вона спустошується раніше за чергу промісів. - Забувати, що
awaitце теж мікрозадача. Код післяawaitне продовжується миттєво, навіть якщо ви чекаєте на вже готове значення.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.