Event Loop
Event Loop
Технический фундамент очередей
Для практики важно различать уровни приоритета задач:
- текущий синхронный стек выполняется первым;
- после опустошения стека выполняются
microtask(Promise.then,queueMicrotask); - затем берется следующая
macrotask(setTimeout, события, I/O callbacks).
Эта последовательность повторяется на каждой итерации event loop и определяет фактический порядок побочных эффектов в приложении.
Зачем понимать Event Loop
Ты уже видел, что асинхронные задачи выполняются "потом". Event Loop объясняет, что значит это "потом" на практике. Без этой модели сложно предсказывать порядок вывода, поведение setTimeout, очередность Promise-обработчиков и реакцию UI.
Ключевой момент: Event Loop это механизм, который координирует выполнение задач в JavaScript-окружении.
Проверь себя: почему знание Event Loop помогает дебажить "странный" порядок логов?
Базовая модель: Call Stack, Web APIs, Queues
Простая схема:
Call Stack- текущий стек вызовов, где выполняется синхронный код.Web APIs/окружение - место, где живут таймеры, сетевые операции, события.Queue- очередь задач, готовых к выполнению.Event Loop- проверяет, пуст ли стек, и переносит следующую задачу из очереди в стек.
Смотри, что важно: задача из очереди не может выполняться, пока стек занят синхронным кодом.
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
Вывод будет 1, 3, 2. Даже при 0ms колбэк не "вклинивается" в середину текущего синхронного блока.
Macro task и micro task
В Event Loop есть несколько типов очередей, но на старте важно понимать разницу:
- Macro task:
setTimeout,setInterval, события. - Micro task:
Promise.then/catch/finally,queueMicrotask.
После завершения текущего синхронного кода сначала очищается очередь microtask, и только потом берется следующая macro task.
console.log('A');
setTimeout(() => console.log('B: timeout'), 0);
Promise.resolve().then(() => console.log('C: promise'));
console.log('D');
Порядок: A, D, C: promise, B: timeout.
Здесь часто путаются: обещание (Promise) срабатывает раньше таймера, потому что microtask имеет более высокий приоритет в этом цикле.
Проверь себя: что изменится, если убрать Promise.resolve().then(...)?
Реальные сценарии, где это всплывает
- UI-события и обновление состояния.
Если ты ставишь несколько асинхронных действий подряд, Event Loop определяет, в каком порядке они изменят интерфейс.
- Обработка данных после API.
- приходит ответ;
- ты запускаешь обработку через
Promise; - в этот же момент есть таймер-анимация;
- порядок их выполнения влияет на то, что увидит пользователь.
- Производительность.
Длинный синхронный блок блокирует стек, и пока он не закончится, Event Loop не отдаст ход никаким очередям. Из-за этого интерфейс "фризит".
Типичная ошибка: тяжелая синхронная работа
setTimeout(() => console.log('timeout done'), 0);
for (let i = 0; i < 1e9; i++) {
// тяжелая блокирующая работа
}
console.log('sync done');
Колбэк таймера выполнится только после цикла. Это частый edge case: разработчик думает, что setTimeout(0) выполнится "сразу", но стек еще занят.
Анти-провал: длинные операции разбивай на части или выноси из критического UI-потока.
Мини-ментальная модель для отладки
Когда видишь непонятный порядок:
- Отметь, что синхронно (идет сразу в стек).
- Отметь
microtask(Promise-обработчики). - Отметь
macro task(setTimeout, события). - Прогони в голове: sync -> microtasks -> next macrotask.
Эта простая модель решает большинство начальных вопросов по асинхронному порядку.
Проверь себя: в какой момент Event Loop берет задачу из очереди - до очистки стека или после?
Дополнительный пример: явный queueMicrotask и setTimeout в одной точке.
console.log('sync-start');
queueMicrotask(() => console.log('microtask'));
setTimeout(() => console.log('macrotask-timeout'), 0);
console.log('sync-end');
Пошаговая трассировка одного цикла Event Loop
Чтобы тема стала предсказуемой, полезно "прогонять" код шагами:
- Выполняется весь синхронный код текущего тика.
- Очищается очередь microtask до конца.
- Берется одна macrotask и выполняется.
- После нее снова очищается microtask.
- Цикл повторяется.
console.log('S1');
setTimeout(() => {
console.log('M1');
Promise.resolve().then(() => console.log('m-from-M1'));
}, 0);
Promise.resolve().then(() => console.log('m1'));
console.log('S2');
Ожидаемый порядок:
S1(sync)S2(sync)m1(microtask после sync)M1(следующая macrotask)m-from-M1(microtask после выполнения macrotask)
Ключевая идея: у каждой macrotask есть "хвост" из microtask, который выполняется перед переходом к следующей macrotask.
Вложенные microtask: почему они могут "вытеснять" таймеры
Microtask-очередь очищается полностью. Если microtask порождает новый microtask, он тоже выполнится до перехода к macrotask.
console.log('start');
Promise.resolve().then(() => {
console.log('micro-1');
Promise.resolve().then(() => console.log('micro-2'));
});
setTimeout(() => console.log('timeout'), 0);
console.log('end');
Порядок будет:
startendmicro-1micro-2timeout
Это важный момент для UI: длинные цепочки microtask могут задерживать визуальные обновления и обработку событий.
Смешанный сценарий: setTimeout и Promise внутри одного колбэка
Новички часто теряются, когда в macrotask создаются новые microtask и macrotask.
setTimeout(() => {
console.log('T1');
Promise.resolve().then(() => console.log('m-in-T1'));
setTimeout(() => console.log('T2'), 0);
}, 0);
Что происходит:
- выполняется
T1(первая macrotask); - после завершения
T1сразу выполняетсяm-in-T1(microtask); - только потом очередь дойдет до
T2(следующая macrotask).
Правило для дебага: внутри macrotask всегда отдельно отслеживай созданные microtask, они сработают раньше следующего таймера.
Практика производительности: разбиение тяжелой задачи на чанки
Если работа тяжелая, лучше выполнять ее частями, чтобы event loop мог обрабатывать интерфейс между кусками.
function processInChunks(total, chunkSize) {
let index = 0;
function runChunk() {
const end = Math.min(index + chunkSize, total);
while (index < end) {
index++;
// имитация тяжелой работы
}
if (index < total) {
setTimeout(runChunk, 0);
} else {
console.log('Готово без длинной блокировки');
}
}
runChunk();
}
processInChunks(1_000_000, 50_000);
Это не "ускоряет" вычисления само по себе, но уменьшает длинные стоп-кадры интерфейса и улучшает отзывчивость.
Частые инженерные ошибки в теме Event Loop
- Думать, что
setTimeout(..., 0)значит "прямо сейчас". - Не различать источник задачи: sync/microtask/macrotask.
- Ставить тяжелую синхронную обработку в обработчик клика без разбиения.
- Игнорировать, что
Promise.thenможет изменить порядок эффектов по сравнению с таймерами.
Анти-провал: при любом "странном порядке" подпиши каждый шаг меткой и явно классифицируй его тип задачи.
Краткий итог
Event Loopуправляет порядком выполнения асинхронных задач.- Пока
Call Stackне пуст, задачи из очередей не стартуют. Promise-обработчики (microtask) обычно выполняются раньшеsetTimeout(macro task).setTimeout(0)не делает колбэк синхронным.- Понимание очередей помогает предсказывать поведение кода и быстрее отлаживать баги порядка.