Событийный цикл (Event Loop): порядок выполнения

Самый частый и самый важный вопрос про асинхронность.

Event Loop — механизм, который выполняет синхронный код, а затем по очереди разбирает отложенные задачи: сначала все микрозадачи (Promise), потом одну макрозадачу (setTimeout).

Три участника

  • Call stack — стек, где выполняется синхронный код прямо сейчас.
  • Очередь микрозадач (microtasks) — коллбэки промисов (.then, await).
  • Очередь макрозадач (macrotasks)setTimeout, setInterval, события.

Правило: сначала выполняется весь синхронный код. Затем event loop опустошает всю очередь микрозадач и только потом берёт одну макрозадачу — и снова проверяет микрозадачи.

Классический пример

console.log("1: синхронный старт");

setTimeout(() => console.log("2: setTimeout (macro)"), 0);

Promise.resolve().then(() => console.log("3: promise (micro)"));

console.log("4: синхронный конец");

Вывод:

1: синхронный старт
4: синхронный конец
3: promise (micro)
2: setTimeout (macro)

Сначала идёт весь синхронный код (1 и 4). Потом микрозадача — промис (3). И только затем макрозадача — setTimeout (2), даже с задержкой 0.

Микрозадачи опустошаются полностью

Несколько .then в цепочке отработают до любого setTimeout, даже если таймеров несколько.

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve()
  .then(() => console.log("C"))
  .then(() => console.log("D"));
setTimeout(() => console.log("E"), 0);
console.log("F");

Вывод:

A
F
C
D
B
E

Порядок: синхронные A и F; затем вся очередь микрозадач — C, D; затем макрозадачи по очереди — B, E.

Почему так сделано

Микрозадачи имеют приоритет, чтобы цепочки промисов завершались «одним целым» до того, как браузер займётся отрисовкой или таймерами. Это делает поведение Promise предсказуемым.

Как объяснить это на собеседовании

Опорная фраза: «JavaScript выполняет один кусок синхронного кода до конца (run-to-completion), а потом event loop разбирает отложенные задачи». Важно подчеркнуть два уровня очередей: микрозадачи (промисы, queueMicrotask, await) опустошаются полностью, и только затем берётся одна макрозадача (setTimeout, события, setInterval) — после чего снова проверяются микрозадачи. Именно из-за этого setTimeout(fn, 0) не означает «выполнить немедленно»: задержка 0 лишь ставит задачу в очередь макрозадач, позади всех уже накопленных микрозадач.

Частая ловушка-продолжение: бесконечная генерация микрозадач (например, рекурсивный Promise.resolve().then(...)) может «заморозить» отрисовку, потому что браузер не дойдёт до макрозадач и рендера, пока очередь микрозадач не опустеет. Понимание этого отличает кандидата, который просто заучил порядок, от того, кто понимает механику.

Итог

  • Сначала весь синхронный код, потом микрозадачи, потом одна макрозадача — и цикл повторяется.
  • Promise (микро) всегда выполняется раньше setTimeout (макро), даже при задержке 0.
  • Очередь микрозадач опустошается целиком перед каждой макрозадачей.
Проверьте себя
1. Что выполнится раньше: коллбэк Promise.then или setTimeout(fn, 0)?
AsetTimeout, у него задержка 0
BPromise.then — микрозадачи приоритетнее макрозадач
CОни выполнятся одновременно
DЗависит от браузера случайным образом
2. В каком порядке выполнится: console.log(1); setTimeout(()=>log(2)); Promise.resolve().then(()=>log(3)); console.log(4)?
A1 2 3 4
B1 4 3 2
C1 4 2 3
D4 3 2 1
3. Сколько микрозадач выполняется перед следующей макрозадачей?
AРовно одна
BВся очередь микрозадач опустошается целиком
CПоловина очереди
DНи одной
Поддержать проект