← Назад к списку
ТехническаяFrontendMiddle

Как работает событийный цикл (event loop) в JavaScript? Чем микрозадачи отличаются от макрозадач?

Короткий ответ

  • JavaScript однопоточный, конкурентность даёт событийный цикл
  • Синхронный код выполняется до конца, очищая стек вызовов
  • Макрозадачи: setTimeout, setInterval, события ввода, сообщения
  • Микрозадачи: колбэки промисов, queueMicrotask, MutationObserver
  • Все микрозадачи выполняются до следующей макрозадачи
  • Рендеринг страницы происходит между макрозадачами
  • Длинная задача блокирует и рендер, и обработку событий

Событийный цикл по очереди выполняет задачи из очередей, причём очередь микрозадач полностью опустошается перед каждой следующей макрозадачей и перед рендером.

Как сказать вслух

пример ответа

JavaScript выполняет код в одном потоке, а асинхронность устроена через очереди задач. Когда стек вызовов пуст, движок берёт следующую задачу из очереди. Важно, что колбэки промисов — это микрозадачи, и они выполняются все сразу, раньше, чем setTimeout. Поэтому порядок вывода в примерах с промисами и таймерами именно такой, а долгие вычисления блокируют отрисовку страницы.

Подробный ответ

Основной ответ

Движок JavaScript выполняет код в одном потоке: пока стек вызовов не пуст, ничего другого не происходит. Браузер держит несколько очередей. В очередь макрозадач попадают колбэки setTimeout и setInterval, обработчики событий, сетевые колбэки. В очередь микрозадач — реакции промисов (then/catch/finally), queueMicrotask и MutationObserver. Алгоритм такой: выполнить одну макрозадачу, затем полностью опустошить очередь микрозадач (включая те, что добавились по ходу), затем браузер при необходимости делает рендер, и цикл повторяется. Отсюда следствия: Promise.resolve().then(...) сработает раньше setTimeout(..., 0), а бесконечное добавление микрозадач способно «заморозить» страницу, потому что до рендера дело не дойдёт.

Ключевые моменты

  • Один поток. Весь пользовательский JS и рендер делят один главный поток, поэтому тяжёлая синхронная работа подвешивает интерфейс.
  • Приоритет микрозадач. Очередь микрозадач опустошается целиком после каждой макрозадачи, поэтому промисы всегда опережают таймеры.
  • Рендер между задачами. Браузер отрисовывает кадр между макрозадачами; requestAnimationFrame вызывается прямо перед отрисовкой.
  • async/await — это промисы. Код после await продолжается как микрозадача, это тот же механизм, что и then.

Практический контекст

Понимание цикла нужно при отладке «почему состояние обновилось не тогда», при оптимизации долгих задач (разбивка через setTimeout, scheduler.yield, воркеры) и в вопросах про порядок вывода console.log. Интервьюер обычно даёт сниппет с setTimeout и промисами и просит назвать порядок, а затем спрашивает, почему. Сильный ответ связывает теорию с практикой: долгие задачи дробят, тяжёлые вычисления уносят в Web Worker.

Пример кода

console.log('1');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('2');
// Вывод: 1, 2, 3, 4 — микрозадача промиса раньше таймера

Частые ошибки

  • Говорят, что setTimeout(fn, 0) выполнится «сразу», забывая про очередь и минимальную задержку
  • Путают местами микро- и макрозадачи или называют промисы макрозадачами
  • Не упоминают, что рендер тоже живёт в этом цикле и блокируется длинными задачами

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru