Event Loop

Что такое event loop?

Event loop — механизм, который координирует выполнение кода, управляя очередями задач.

Схема работы:

Call Stack
    ↓ (пуст?)
Microtask Queue  → выполнить все
    ↓
Macrotask Queue  → взять одну задачу
    ↓
Microtask Queue  → выполнить все
    ↓
... и так по кругу

JavaScript однопоточный — в каждый момент выполняется только одна задача.


Что такое call stack?

Стек вызовов — структура данных LIFO (последний вошёл — первый вышел).

function a() { b(); }
function b() { c(); }
function c() { console.log('hello'); }
a();
 
// Call stack:
// c() ← выполняется
// b()
// a()
// main()

Пока стек не очищен — никакие колбэки из очередей не выполняются.


Что относится к microtasks, а что к macrotasks?

Microtasks (высокий приоритет):

  • Promise.then / .catch / .finally
  • queueMicrotask(fn)
  • MutationObserver
  • async/await (под капотом — Promise)

Macrotasks (низкий приоритет):

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • UI-события (click, input)

Каков порядок выполнения sync → microtasks → macrotasks?

Алгоритм:

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

Ключевое: microtasks дренируются целиком после каждой macrotask, а не по одной.


Почему Promise выполняется раньше setTimeout(…, 0)?

setTimeout(() => console.log('timeout'), 0); // macrotask
Promise.resolve().then(() => console.log('promise')); // microtask
 
// Вывод:
// promise  ← microtask выполняется раньше
// timeout  ← macrotask выполняется после

setTimeout(..., 0) — не «немедленно», а «добавить в macrotask queue как можно скорее».


Классический пример с подвохом

console.log('1');
 
setTimeout(() => console.log('2'), 0);
 
Promise.resolve()
  .then(() => console.log('3'))
  .then(() => console.log('4'));
 
console.log('5');
ШагЧто происходитВывод
syncconsole.log('1')1
syncРегистрируется setTimeout → macrotask
syncРегистрируется Promise.then
syncconsole.log('5')5
microtask.then(() => '3')3
microtask.then(() => '4')4
macrotasksetTimeout callback2

Итого: 1 → 5 → 3 → 4 → 2


Что важно помнить про очередь microtasks?

Если microtask создаёт новую microtask — она тоже выполняется до следующей macrotask.

Promise.resolve()
  .then(() => {
    console.log('A');
    return Promise.resolve();
  })
  .then(() => console.log('B'));
 
setTimeout(() => console.log('C'), 0);
 
// A → B → C

Бесконечные цепочки промисов могут заблокировать выполнение macrotasks.


Как коротко ответить про event loop на собеседовании?

JavaScript — однопоточный язык. Все задачи выполняются через call stack. После выполнения синхронного кода движок дренирует очередь microtasks (Promise, MutationObserver), затем берёт одну macrotask (setTimeout, setInterval), снова дренирует microtasks — и так по кругу.


В чём разница между requestAnimationFrame и setTimeout?

requestAnimationFrame (rAF)setTimeout / setInterval
ПривязкаК циклу отрисовки браузера (обычно ~60 Гц, может совпадать с дисплеем)К очереди таймеров (macrotask), не синхронизирован с кадром
Пауза вкладкиЧасто приостанавливается в фоне — экономия CPUМожет продолжать тикать (зависит от браузера и минимальной задержки)
Типичное использованиеАнимации, измерение перед следующей отрисовкойПроизвольная отложенная логика, поллинг, дебаунс «не привязанный к кадру»
Аргумент времениОдин колбэк на кадр; точное «через N мс» не задаётсяЯвная задержка в мс (реальная не гарантирована)

Для собеса: для плавной анимации и обновления визуала перед paint предпочтительнее rAF; setTimeout(0) — не гарантия «сразу после текущего кадра», а отложенная macrotask.

См. также