← Назад к вопросам
Как Event Loop добавляет micro tasks в call stack?
1.7 Middle🔥 291 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Event Loop и micro tasks: как это работает
Основная концепция
Event Loop — это механизм в JavaScript, который управляет выполнением кода. Он постоянно проверяет, есть ли задачи в очередях (Call Stack, Callback Queue, Microtask Queue) и выполняет их по определённому порядку.
Структура Event Loop
Call Stack
├─ Синхронный код (выполняется сразу)
├─ Функции
├─ Операции
└─ Удаляется при return
Microtask Queue
├─ Promise callbacks (.then, .catch, .finally)
├─ async/await
├─ queueMicrotask()
├─ MutationObserver
└─ Выполняется после каждой синхронной операции
Callback Queue (Macrotask Queue)
├─ setTimeout
├─ setInterval
├─ setImmediate
├─ I/O операции
├─ UI events (click, scroll)
└─ Выполняется после microtask queue
Порядок выполнения
// 1. Выполняем весь синхронный код
console.log('1. Start');
// 2. Добавляем Promise в microtask queue
Promise.resolve()
.then(() => console.log('2. Promise (microtask)'));
// 3. Добавляем setTimeout в callback queue
setTimeout(() => {
console.log('4. setTimeout (macrotask)');
}, 0);
// 4. Продолжаем синхронный код
console.log('3. End');
// Вывод:
// 1. Start
// 3. End
// 2. Promise (microtask)
// 4. setTimeout (macrotask)
Почему Promise выполняется ДО setTimeout?
Потому что Event Loop сначала выполняет все microtasks, потом все macrotasks.
Этап 1: Выполняем Call Stack
console.log('1. Start') ✓
console.log('3. End') ✓
Этап 2: Call Stack пуст, проверяем Microtask Queue
Promise.then() ✓
Этап 3: Microtask Queue пуста, проверяем Callback Queue
setTimeout() ✓
Детальный процесс
console.log('Начало');
// Микротаск 1
Promise.resolve()
.then(() => {
console.log('Promise 1');
// Если добавить микротаск ВО ВРЕМЯ микротаска
return Promise.resolve()
.then(() => console.log('Promise 1.1'));
});
// Макротаск 1
setTimeout(() => {
console.log('setTimeout 1');
// Если добавить микротаск во время макротаска
Promise.resolve()
.then(() => console.log('Promise (в setTimeout)'));
}, 0);
// Микротаск 2
Promise.resolve()
.then(() => console.log('Promise 2'));
console.log('Конец');
// Вывод:
// Начало
// Конец
// Promise 1
// Promise 1.1 ← Даже вложенный Promise выполнится перед setTimeout!
// Promise 2
// setTimeout 1
// Promise (в setTimeout)
Микротаски vs Макротаски
| Тип | Примеры | Когда выполняется |
|---|---|---|
| Microtask | Promise, async/await, queueMicrotask(), MutationObserver | После каждого макротаска или синхронного кода |
| Macrotask | setTimeout, setInterval, setImmediate, I/O, DOM events | После выполнения всех микротасков |
Пример с async/await
// async/await это просто синтаксис для Promise
async function test() {
console.log('1. In async');
await Promise.resolve();
console.log('2. After await');
}
console.log('0. Start');
test();
console.log('3. End');
// Вывод:
// 0. Start
// 1. In async
// 3. End
// 2. After await ← Выполняется в микротаск очереди
Реальный пример: Race Condition
// ❌ Проблема: Event Loop может вызвать проблемы
let count = 0;
// Макротаск: setTimeout
setTimeout(() => {
count++;
console.log('setTimeout:', count);
}, 0);
// Микротаск: Promise
Promise.resolve().then(() => {
count++;
console.log('Promise:', count);
});
// Вывод:
// Promise: 1 ← Выполнится первым!
// setTimeout: 2
// ✅ Решение: используй один тип
Promise.resolve().then(() => {
count++;
console.log('Promise 1:', count);
});
Promise.resolve().then(() => {
count++;
console.log('Promise 2:', count);
});
// Вывод:
// Promise 1: 1
// Promise 2: 2
// Порядок предсказуем
Как добавлять микротаски вручную
// Способ 1: Promise
Promise.resolve()
.then(() => console.log('Micro 1'));
// Способ 2: async/await
(async () => {
await Promise.resolve();
console.log('Micro 2');
})();
// Способ 3: queueMicrotask (явно)
queueMicrotask(() => {
console.log('Micro 3');
});
// Способ 4: MutationObserver (legacy)
const observer = new MutationObserver(() => {
console.log('Micro 4 (MutationObserver)');
});
observer.observe(document.body, { childList: true });
document.body.appendChild(document.createElement('div'));
Визуализация Event Loop
console.log('Шаг 1: Синхронный код');
setTimeout(() => {
console.log('Шаг 5: Макротаск');
}, 0);
Promise.resolve()
.then(() => {
console.log('Шаг 3: Микротаск');
// Вложенный микротаск
queueMicrotask(() => {
console.log('Шаг 4: Вложенный микротаск');
});
});
console.log('Шаг 2: Синхронный код (продолжение)');
// Timeline:
// t=0: Выполняем синхронный код
// Шаг 1
// Шаг 2
// t=0+: Проверяем Microtask Queue
// Шаг 3
// Шаг 4
// t=0++: Проверяем Callback Queue
// Шаг 5
// Вывод:
// Шаг 1
// Шаг 2
// Шаг 3
// Шаг 4
// Шаг 5
Практическое применение: Batch обновления
// Пример: обновляем DOM, но хотим сделать это батчем
let updates = [];
let scheduled = false;
function scheduleUpdate(callback) {
updates.push(callback);
if (!scheduled) {
scheduled = true;
// Используем микротаск для батчинга
queueMicrotask(() => {
updates.forEach(cb => cb());
updates = [];
scheduled = false;
});
}
}
// Использование
scheduleUpdate(() => {
console.log('Обновление 1');
});
scheduleUpdate(() => {
console.log('Обновление 2');
});
scheduleUpdate(() => {
console.log('Обновление 3');
});
// Все выполнятся в одной микротаск очереди
// Обновление 1
// Обновление 2
// Обновление 3
Ошибки с Event Loop
// ❌ Ошибка: забыли про микротаски
function process() {
// Пользователь ждёт синхронный результат
return new Promise(resolve => {
resolve('data');
});
}
const result = process(); // ❌ undefined! (Promise, а не данные)
// ✅ Правильно
process().then(result => {
console.log(result); // 'data'
});
// ❌ Ошибка: setTimeout вместо микротаска
function notify() {
setTimeout(() => {
// Может быть очень задержано
console.log('notification');
}, 0);
}
// ✅ Правильно: микротаск для уведомлений
function notify() {
queueMicrotask(() => {
console.log('notification'); // Сразу после синхронного кода
});
}
Итоги
Event Loop работает так:
1. Выполняй весь синхронный код (Call Stack)
2. Когда Call Stack пуст:
a. Выполняй ВСЕ микротаски (Microtask Queue)
b. Выполняй ОДИН макротаск (Callback Queue)
c. Перейти к пункту 2
Микротаски ВСЕГДА выполняются перед макротасками, поэтому Promise выполнится раньше setTimeout, даже если setTimeout имеет задержку 0.