Как работает Event Loop в JavaScript: задачи и микрозадачи
Продвинутое3 минРедакция Innovialabs
Коротко
JavaScript выполняет код в одном потоке: сначала синхронный код, затем все микрозадачи (промисы, await), и только потом следующая задача (setTimeout, события).
Короткий ответ
JavaScript выполняет код в одном потоке. Event Loop — это механизм, который решает, что выполнять дальше. Правило одно:
- Выполнить текущую задачу (macrotask) до конца — например, весь скрипт.
- Выполнить все микрозадачи из очереди, включая те, что добавились по ходу.
- Дать браузеру шанс отрисовать страницу.
- Взять следующую задачу и повторить.
Отсюда главный вывод: Promise.then всегда выполнится раньше setTimeout(..., 0), если оба запланированы в одной задаче.
Три участника: стек, задачи, микрозадачи
- Call stack — стек вызовов. Пока в нём что-то есть, ничего другого не выполняется.
- Очередь задач (task queue) — сюда попадают колбэки
setTimeout,setInterval, события DOM, сетевые ответы, сообщенияpostMessage. - Очередь микрозадач (microtask queue) — колбэки
.then/.catch/.finally, продолжение функции послеawait,queueMicrotask,MutationObserver.
| Источник | Очередь |
|---|---|
setTimeout, setInterval | задачи |
| клик, ввод, загрузка | задачи |
Promise.then, await | микрозадачи |
queueMicrotask | микрозадачи |
Важно: setTimeout(fn, 0) не означает «сразу». Это «не раньше, чем через 0 мс и только когда стек пуст и микрозадачи закончились».
Пример 1: базовый порядок
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
Вывод: 1, 4, 3, 2. Синхронный код идёт первым (1, 4), затем микрозадача (3), затем следующая задача (2).
Пример 2: async/await
async function a() {
console.log('a1');
await null;
console.log('a2');
}
console.log('start');
setTimeout(() => console.log('timeout'), 0);
a();
Promise.resolve().then(() => console.log('then'));
console.log('end');
Вывод: start, a1, end, a2, then, timeout.
Ключевой момент: код async-функции до первого await выполняется синхронно. Всё, что после await, становится микрозадачей. Она попала в очередь раньше, чем then, поэтому a2 выводится первым.
Пример 3: вложенные микрозадачи
setTimeout(() => console.log('t1'), 0);
Promise.resolve().then(() => {
console.log('p1');
setTimeout(() => console.log('t2'), 0);
Promise.resolve().then(() => console.log('p2'));
});
Вывод: p1, p2, t1, t2. Микрозадача, добавленная внутри микрозадачи, выполняется в том же цикле — очередь опустошается полностью. Новый setTimeout встаёт в конец очереди задач, после t1.
Как решать такие задачи на собеседовании
- Выпишите весь синхронный вывод сверху вниз.
- Отметьте, что ушло в микрозадачи, в порядке постановки.
- Отметьте, что ушло в задачи.
- После синхронного кода выполните все микрозадачи, добавляя новые в конец.
- Берите задачи по одной, после каждой снова опустошайте микрозадачи.
Частые ошибки и практические последствия
- Думать, что
awaitблокирует поток. Он приостанавливает только текущую функцию, остальной код продолжает работать. - Бесконечные микрозадачи. Если микрозадача постоянно ставит новую, браузер не дойдёт до отрисовки и страница зависнет.
- Тяжёлые вычисления в стеке. Долгий цикл блокирует клики и анимации. Разбивайте работу на части или выносите в Web Worker.
- Расчёт на точность таймеров. Задержка
setTimeout— минимальная, а не гарантированная.
В Node.js у цикла есть собственные фазы и process.nextTick, но правило «микрозадачи до следующей задачи» работает так же.