Skip to main content

Мікрозадача проміса

Мікрозадача (microtask) це невелика задача, яка виконується після поточного стека викликів, але до того, як рушій перейде до макрозадач (setTimeout, setInterval, події DOM). Обробники проміса завжди потрапляють саме в чергу мікрозадач, тому й виконуються раніше за таймери.

Теорія

TL;DR

  • Мікрозадачі це «черга термінових задач», яка спустошується одразу після поточного синхронного коду.
  • .then(), .catch(), .finally() не викликаються синхронно: вони ставляться в чергу мікрозадач.
  • Після кожної макрозадачі рушій виконує всі мікрозадачі, і лише потім бере наступну макрозадачу.
  • Мікрозадачі виконуються в порядку додавання, а нова мікрозадача, створена під час обробки черги, виконається в тому ж проході.
  • Тому колбек проміса завжди випереджає setTimeout(..., 0).

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

javascript
console.log('1'); Promise.resolve().then(() => console.log('2')); console.log('3');

Результат:

javascript
1 3 2

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

  1. Спершу виконується весь синхронний код (у стеку викликів).
  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 обробляє події так:

  1. Виконує весь синхронний код (Call Stack).
  2. Виконує всі мікрозадачі, доки черга не спорожніє.
  3. Переходить до однієї макрозадачі (наприклад, setTimeout).
  4. Знову виконує всі мікрозадачі, що накопичилися після неї.
  5. І так по колу.

Порівняння з макрозадачами:

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

Вивід:

javascript
A D C B

Чому:

  1. A, D синхронно;
  2. C мікрозадача (від проміса);
  3. B макрозадача (від setTimeout).

Мікрозадача C виконується раніше за таймер B.

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

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

Порядок усередині черги

Кілька мікрозадач виконуються строго в порядку додавання:

javascript
Promise.resolve().then(() => console.log('1')); Promise.resolve().then(() => console.log('2')); Promise.resolve().then(() => console.log('3')); console.log('4');

Вивід:

javascript
4 1 2 3

Усі .then() потрапляють в одну чергу мікрозадач і виконуються в порядку додавання, після завершення синхронного коду (4).

Мікрозадача, створена всередині іншої мікрозадачі, теж виконається в цьому ж проході:

javascript
Promise.resolve().then(() => { console.log('A'); Promise.resolve().then(() => console.log('B')); });

Вивід:

javascript
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 не продовжується миттєво, навіть якщо ви чекаєте на вже готове значення.

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

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

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