Event Loop
Event Loop — механизм, который координирует выполнение кода, обработку событий и выполнение отложенных задач в однопоточном JavaScript.
В Node Event Loop работает иначе из-за libuv: 6 фаз, process.nextTick, setImmediate, Thread Pool. Этот файл — про браузерный JS-контекст (HTML5 spec).
Зачем нужно
JavaScript выполняется в одном потоке, но может обрабатывать тысячи асинхронных операций. Event Loop — сердце этой модели. Без понимания Event Loop невозможно предсказать порядок выполнения асинхронного кода.
Где используется
- Обработка событий (клики, ввод)
- Выполнение таймеров (setTimeout, setInterval)
- Обработка HTTP-ответов (fetch)
- Promise-цепочки
- Анимации (requestAnimationFrame)
Предпосылки
Компоненты 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. Бесконечная цепочка микрозадач
// Никогда не создавай бесконечную рекурсию микрозадач
// Это заблокирует и рендер, и макрозадачи
Практика
- Предскажи порядок вывода для 5 разных комбинаций console.log, setTimeout, Promise.then
- Напиши код, демонстрирующий приоритет микрозадач над макрозадачами
- Покажи, как requestAnimationFrame вписывается в Event Loop
- Создай пример, где 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 не существует ничего, что могло бы положить что-то в эту очередь.»
- Цитата:
«Спрашивать порядок выполнения асинхронного кода — абсурд: ответ зависит даже от железа и версии хоста.»