Event Loop

Event Loop — механизм, который координирует выполнение кода, обработку событий и выполнение отложенных задач в однопоточном JavaScript.

См. также Event Loop в Node

В Node Event Loop работает иначе из-за libuv: 6 фаз, process.nextTick, setImmediate, Thread Pool. Этот файл — про браузерный JS-контекст (HTML5 spec).

Зачем нужно

JavaScript выполняется в одном потоке, но может обрабатывать тысячи асинхронных операций. Event Loop — сердце этой модели. Без понимания Event Loop невозможно предсказать порядок выполнения асинхронного кода.

Где используется

  • Обработка событий (клики, ввод)
  • Выполнение таймеров (setTimeout, setInterval)
  • Обработка HTTP-ответов (fetch)
  • Promise-цепочки
  • Анимации (requestAnimationFrame)

Предпосылки

_MOC Асинхронность, Scope

Компоненты Event Loop

1. Call Stack (Стек вызовов)

// Call Stack — структура LIFO (Last In, First Out)
// Каждый вызов функции добавляет фрейм в стек

function third() {
  console.log('third');
}

function second() {
  third;
  console.log('second');
}

function first() {
  second;
  console.log('first');
}

first;

// Стек:
// 1. first помещается в стек
// 2. second помещается в стек
// 3. third помещается в стек
// 4. console.log('third') выполняется → 'third'
// 5. third снимается со стека
// 6. console.log('second') выполняется → 'second'
// 7. second снимается со стека
// 8. console.log('first') выполняется → 'first'
// 9. first снимается со стека
// Вывод: third, second, first

2. Web APIs (Браузерные API)

// Браузер предоставляет API, работающие вне JS-потока:
// - setTimeout / setInterval
// - fetch / XMLHttpRequest
// - DOM Events (addEventListener)
// - Geolocation API
// - Web Workers

// Когда JS вызывает эти API, браузер обрабатывает их
// в отдельных потоках и помещает callback в очередь
// когда операция завершена

3. Task Queue (Macrotask Queue)

// Макрозадачи помещаются сюда после завершения:
// - setTimeout / setInterval callbacks
// - DOM Events (click, input, load)
// - I/O операции
// - requestAnimationFrame (отдельная очередь, перед repaint)

// Event Loop берёт ОДНУ макрозадачу за итерацию

4. Microtask Queue

// Микрозадачи имеют ПРИОРИТЕТ над макрозадачами
// Все микрозадачи выполняются после текущей задачи,
// ДО следующей макрозадачи

// Источники микрозадач:
// - Promise.then / catch / finally
// - queueMicrotask
// - MutationObserver
// - await (после возобновления)

Алгоритм Event Loop

┌─────────────────────────────────────┐
│           Event Loop                │
│                                     │
│  1. Выполнить весь синхронный код   │
│     (опустошить Call Stack)         │
│                                     │
│  2. Выполнить ВСЕ микрозадачи      │
│     (Microtask Queue)              │
│     (если появились новые —        │
│      выполнить и их тоже)          │
│                                     │
│  3. Render (если нужно):           │
│     - requestAnimationFrame        │
│     - Style calculation            │
│     - Layout                       │
│     - Paint                        │
│                                     │
│  4. Взять ОДНУ макрозадачу         │
│     (Task Queue)                   │
│                                     │
│  5. Goto 1                         │
└─────────────────────────────────────┘

Примеры порядка выполнения

Пример 1: Базовый

console.log('1'); // Синхронный

setTimeout( => console.log('2'), 0); // Макрозадача

Promise.resolve.then( => console.log('3')); // Микрозадача

console.log('4'); // Синхронный

// Порядок вывода: 1, 4, 3, 2
// 1. Синхронный код: '1', '4'
// 2. Микрозадачи: '3' (Promise.then)
// 3. Макрозадачи: '2' (setTimeout)

Пример 2: Вложенные микрозадачи

console.log('start');

setTimeout( => console.log('timeout'), 0);

Promise.resolve
  .then(() => {
    console.log('promise 1');
    return Promise.resolve;
  })
  .then( => console.log('promise 2'));

Promise.resolve.then( => console.log('promise 3'));

console.log('end');

// Вывод: start, end, promise 1, promise 3, promise 2, timeout
// promise 2 после promise 3, потому что .then создаёт новую микрозадачу

Пример 3: Полный пример

console.log('script start');

setTimeout(function {
  console.log('setTimeout');
}, 0);

Promise.resolve
  .then(function {
    console.log('promise1');
  })
  .then(function {
    console.log('promise2');
  });

requestAnimationFrame(function {
  console.log('rAF');
});

console.log('script end');

// Гарантированный порядок:
// script start → script end → promise1 → promise2
// setTimeout и rAF — порядок зависит от браузера и тайминга
// Обычно: script start, script end, promise1, promise2, rAF, setTimeout

Пример 4: Микрозадачи блокируют рендер

// Опасно! Микрозадачи выполняются ВСЕ перед рендером
function blockingMicrotasks() {
  let count = 0;
  function addMicrotask() {
    if (count < 100000) {
      count++;
      queueMicrotask(addMicrotask); // Бесконечные микрозадачи!
    }
  }
  addMicrotask;
  // Браузер не отрисует ничего, пока не завершатся все 100000 микрозадач
}

Пример 5: async/await в контексте Event Loop

async function asyncFunc() {
  console.log('async start'); // Синхронная часть

  await Promise.resolve; // Здесь функция «приостанавливается»
  // Всё после await — микрозадача

  console.log('async end'); // Микрозадача
}

console.log('script start');
asyncFunc;
console.log('script end');

// Вывод: script start, async start, script end, async end

queueMicrotask

// Явное добавление микрозадачи
console.log('1');

queueMicrotask(() => {
  console.log('microtask');
});

console.log('2');

// Вывод: 1, 2, microtask

// Использование: когда нужно отложить выполнение,
// но выполнить ДО следующего рендера/макрозадачи
queueMicrotask(() => {
  // Обновить состояние перед рендером
});

requestAnimationFrame

// Выполняется перед каждой перерисовкой (обычно 60fps = каждые ~16.6ms)
function animate(timestamp) {
  // Обновить анимацию
  element.style.transform = `translateX(${timestamp / 10}px)`;

  // Запланировать следующий кадр
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

// В Event Loop: после микрозадач, перед макрозадачами
// Синхронный код → Микрозадачи → rAF → Рендер → Макрозадачи

Node.js Event Loop (отличия)

// Node.js имеет 6 фаз:
// 1. timers — setTimeout, setInterval
// 2. pending callbacks — системные callback'и
// 3. idle, prepare — внутренние
// 4. poll — I/O
// 5. check — setImmediate
// 6. close callbacks — socket.on('close')

// setImmediate vs setTimeout(fn, 0)
setTimeout( => console.log('timeout'), 0);
setImmediate( => console.log('immediate'));
// Порядок НЕ гарантирован в главном модуле

// process.nextTick — выполняется ДО микрозадач
process.nextTick( => console.log('nextTick'));
Promise.resolve.then( => console.log('promise'));
// nextTick → promise (nextTick приоритетнее)

Визуальная схема

                    ┌──────────────┐
                    │  Call Stack   │
                    │ ┌──────────┐ │
                    │ │ func   │ │
                    │ └──────────┘ │
                    └──────┬───────┘
                           │
              ┌────────────┴────────────┐
              │                         │
              ▼                         ▼
     ┌────────────────┐     ┌─────────────────┐
     │ Web APIs       │     │ Microtask Queue  │
     │ (Timer, Fetch, │     │ Promise.then   │ ← ПРИОРИТЕТ
     │  DOM Events)   │     │ queueMicrotask │
     └───────┬────────┘     └─────────────────┘
             │
             ▼
     ┌────────────────┐
     │ Task Queue     │
     │ (Macrotasks)   │ ← Одна задача за итерацию
     │ setTimeout cb  │
     │ click handler  │
     └────────────────┘

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

1. setTimeout(fn, 0) не выполняется мгновенно

setTimeout( => console.log('timeout'), 0);
// Минимальная задержка: 4ms (по спецификации для вложенных >5)
// Выполнится только после всех синхронных задач И микрозадач

2. Тяжёлые синхронные операции блокируют всё

// Плохо: блокирует UI на несколько секунд
for (let i = 0; i < 1000000000; i++) { /* ... */ }

// Решение: разбить на части или использовать Web Worker

3. Бесконечная цепочка микрозадач

// Никогда не создавай бесконечную рекурсию микрозадач
// Это заблокирует и рендер, и макрозадачи

Практика

  1. Предскажи порядок вывода для 5 разных комбинаций console.log, setTimeout, Promise.then
  2. Напиши код, демонстрирующий приоритет микрозадач над макрозадачами
  3. Покажи, как requestAnimationFrame вписывается в Event Loop
  4. Создай пример, где setTimeout с 0ms выполняется позже Promise

Связанные темы

Ресурсы


🎓 Источник: Асинхронное программирование и оптимизация для V8

  • 📅 2025-12-20 · YouTube
  • Тезисы:
    • Event Loop браузера описан в стандарте HTML5; Event Loop Node похож, но НЕ такой же.
    • В Node для таймеров выделена отдельная фаза в Event Loop, одна из первых.
    • setTimeout, alert, fetch — это host API, а не часть языка JavaScript. В чистом JS нет ни таймеров, ни event loop.
    • setTimeout в Node возвращает класс с символами поверх реальных таймеров ОС; реальных таймеров ОС на порядок меньше (enroll/unenroll).
  • Цитата:

    «У многих складывается впечатление, что setTimeout — это какая-то часть языка JavaScript. На самом деле setTimeout — это внешняя API, которая реализована совершенно по-разному в браузере и в ноде. То есть в JavaScript вообще setTimeout нет.»

  • Цитата:

    «Нужно всегда уметь отличать, где есть внешний API, а где наш код на языке JavaScript, чтобы понимать, кто ответственен за эти издержки.»


🎓 Источник: Паттерн Reactor — как устроен Event Loop в Node.js

  • 📅 2025-04-14 · YouTube
  • Тезисы:
    • Event Loop в Node — реализация паттерна Reactor (один event-loop поток + неблокирующее I/O через libuv).
    • Reactor демультиплексирует события от множества дескрипторов в одном цикле — масштабируется без потока на соединение.
    • Microtask Queue выгребается до конца перед следующей фазой таймеров/I/O — поэтому process.nextTick и queueMicrotask могут «задушить» event loop.

⚡ Источник: sobes 07 — JavaScript собеседования Event Loop и вся правда о нем · AsForJS

  • 📅 2023-08-27 · YouTube
  • Тезисы:
    • В спецификации ECMAScript Event Loop НЕТ. Это свойство хост-среды. В разных хостах он реализован по-разному.
    • В спеке JS есть три "очереди" задач: Generic Job, Promise Job Queue, TimeOut Job Queue (последняя — для Atomics, не для setTimeout).
    • Task Queue в HTML5 — это Set, а НЕ очередь. Порядок выполнения задач из неё формально не гарантирован.
    • Термина "макротаск" в современной спеке нет. Микротаски (= Promise Job Queue) есть, "макротаск" — народное имя для Task Queue/Set.
    • В Task Queue кладёт ТОЛЬКО внешний API хост-среды (setTimeout, события DOM, fetch, rAF). Сам JS своими средствами этого делать не умеет.
    • Microtask queue выгребается ПОЛНОСТЬЮ после каждого завершённого task — это закреплено стандартом.
    • setTimeout(0) превращается хостом в минимум 4 мс (только для вложенных >= 5 уровней по WHATWG).
    • Промисы работают и без Event Loop — Promise Job Queue существует на уровне ECMAScript, отдельно от хостовых task'ов.
    • JavaScript однопоточный условно: с 2014 (SharedArrayBuffer, Atomics) — треть спеки про параллелизм агентов.
  • Цитата:

    «Promise Queue в спецификации HTML5 обозначается как микротаск. Микротасков существуют. Макротасков не существует в официальной спецификации.»

  • Цитата:

    «Эти задачи появляются исключительно благодаря любому внешнему API. SetTimeout — внешний API. В самом JavaScript не существует ничего, что могло бы положить что-то в эту очередь.»

  • Цитата:

    «Спрашивать порядок выполнения асинхронного кода — абсурд: ответ зависит даже от железа и версии хоста.»