Событийный цикл детально

Как браузер исполняет JS: call stack, microtask- и macrotask-очереди и почему порядок именно такой.

Event loop (событийный цикл) — механизм, который по очереди забирает задачи из очередей и выполняет их в одном потоке, создавая иллюзию параллельности.

Один поток, но не блокирующий

JavaScript исполняется в одном потоке: в каждый момент работает ровно одна функция. Но при этом интерфейс не «висит» во время сетевых запросов и таймеров. Секрет — в том, что долгие операции отдаются окружению (браузеру или Node), а их колбэки возвращаются в JS позже, через очереди. Дирижирует этим event loop.

Чтобы понимать порядок выполнения, держите в голове три сущности:

  • Call stack — стек вызовов. Сюда кладутся вызовы функций; пока он не опустеет, ничего из очередей не запускается.
  • Очередь macrotask (задачи): колбэки setTimeout, setInterval, события I/O.
  • Очередь microtask (микрозадачи): колбэки .then/.catch/.finally у промисов, queueMicrotask, await.

Главное правило порядка

Алгоритм цикла можно описать так:

  1. Выполнить весь синхронный код (опустошить call stack).
  2. Полностью опустошить очередь microtask (включая микрозадачи, которые добавились по ходу).
  3. Взять одну macrotask, выполнить её.
  4. Снова опустошить все microtask. Повторять.

Ключевой вывод: микрозадачи всегда выполняются раньше следующей макрозадачи. Проверим на «что выведет»:

console.log("1: синхронно");
setTimeout(() => console.log("2: setTimeout (macrotask)"), 0);
Promise.resolve().then(() => console.log("3: Promise (microtask)"));
console.log("4: синхронно");

Вывод:

1: синхронно
4: синхронно
3: Promise (microtask)
2: setTimeout (macrotask)

Сначала отработал весь синхронный код (строки 1 и 4). Потом — микрозадача (3). И только в конце — макрозадача из setTimeout (2), хотя задержка была нулевой. Ноль миллисекунд означает «как можно скорее», но после текущего синхронного кода и всех микрозадач.

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

Цепочка .then().then() добавляет микрозадачи одна за другой, и все они отработают до любого таймера:

console.log("старт");
setTimeout(() => console.log("timeout 1"), 0);
Promise.resolve()
  .then(() => console.log("promise 1"))
  .then(() => console.log("promise 2"));
setTimeout(() => console.log("timeout 2"), 0);
console.log("конец");

Вывод:

старт
конец
promise 1
promise 2
timeout 1
timeout 2

Обе микрозадачи (promise 1, promise 2) вышли раньше обоих таймеров, потому что очередь microtask опустошается целиком перед тем, как взять первую macrotask.

Микрозадача, рождённая внутри микрозадачи

Если внутри .then запланировать новый таймер, он попадёт в очередь macrotask и выполнится позже остальных синхронных микрозадач:

setTimeout(() => console.log("A timeout"), 0);
Promise.resolve().then(() => {
  console.log("B promise");
  setTimeout(() => console.log("C timeout внутри promise"), 0);
});
Promise.resolve().then(() => console.log("D promise"));
console.log("E sync");

Вывод:

E sync
B promise
D promise
A timeout
C timeout внутри promise

Порядок: синхронный E; затем обе микрозадачи B и D; затем макрозадача A (она была поставлена раньше); и последней — C, добавленная уже по ходу.

Зачем это знать на практике

  • Тяжёлый синхронный цикл блокирует отрисовку и обработку кликов — поток один. Разбивайте долгие вычисления.
  • Лавина микрозадач (например, рекурсивный queueMicrotask) тоже способна «заморозить» страницу: до отрисовки очередь обязана опустеть.
  • Понимание порядка снимает «магию» багов вида «почему мой console.log вывелся не там».

Итог

  • JS однопоточен; event loop тасует задачи из очередей.
  • Синхронный код → все microtask → одна macrotask → снова все microtask.
  • Промисы (microtask) всегда обгоняют setTimeout (macrotask).
Проверьте себя
1. Что выполнится раньше: колбэк .then() промиса или колбэк setTimeout(fn, 0)?
A.then() промиса (microtask)
BsetTimeout (macrotask)
CОни выполнятся одновременно
DТот, что объявлен ниже в коде
2. Что произойдёт раньше всего в одном тике event loop?
AПервая macrotask
BПервая microtask
CВесь синхронный код (опустошение call stack)
DОтрисовка экрана
3. Сколько macrotask выполняется за один проход, прежде чем снова опустошить microtask?
AВсе сразу
BРовно одна
CДве
DЗависит от приоритета
Поддержать проект