Как работает событийный цикл (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) выполнится «сразу», забывая про очередь и минимальную задержку
- Путают местами микро- и макрозадачи или называют промисы макрозадачами
- Не упоминают, что рендер тоже живёт в этом цикле и блокируется длинными задачами