Перейти к содержанию
Innovialabs

Как работает Event Loop в JavaScript: задачи и микрозадачи

Продвинутое3 мин

Коротко

JavaScript выполняет код в одном потоке: сначала синхронный код, затем все микрозадачи (промисы, await), и только потом следующая задача (setTimeout, события).

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

JavaScript выполняет код в одном потоке. Event Loop — это механизм, который решает, что выполнять дальше. Правило одно:

  1. Выполнить текущую задачу (macrotask) до конца — например, весь скрипт.
  2. Выполнить все микрозадачи из очереди, включая те, что добавились по ходу.
  3. Дать браузеру шанс отрисовать страницу.
  4. Взять следующую задачу и повторить.

Отсюда главный вывод: 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.

Как решать такие задачи на собеседовании

  1. Выпишите весь синхронный вывод сверху вниз.
  2. Отметьте, что ушло в микрозадачи, в порядке постановки.
  3. Отметьте, что ушло в задачи.
  4. После синхронного кода выполните все микрозадачи, добавляя новые в конец.
  5. Берите задачи по одной, после каждой снова опустошайте микрозадачи.

Частые ошибки и практические последствия

  • Думать, что await блокирует поток. Он приостанавливает только текущую функцию, остальной код продолжает работать.
  • Бесконечные микрозадачи. Если микрозадача постоянно ставит новую, браузер не дойдёт до отрисовки и страница зависнет.
  • Тяжёлые вычисления в стеке. Долгий цикл блокирует клики и анимации. Разбивайте работу на части или выносите в Web Worker.
  • Расчёт на точность таймеров. Задержка setTimeout — минимальная, а не гарантированная.

В Node.js у цикла есть собственные фазы и process.nextTick, но правило «микрозадачи до следующей задачи» работает так же.

Частые вопросы

Promise.then. Это микрозадача, а все микрозадачи выполняются до перехода к следующей задаче, к которой относится колбэк setTimeout.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.