Перейти к содержимому

Node.js Event Loop Phases

Node.js event loop (libuv) состоит из шести фаз, и поведение setImmediate, setTimeout(fn,0) и process.nextTick кардинально различается в зависимости от текущей фазы — это один из самых частых вопросов Senior-собеса.

Node.js Event Loop (libuv) — фазы
┌──────────────────────────────────────────────────────┐
│                                                      │
│   ┌───────────┐                                      │
│   │  timers   │  setTimeout / setInterval callbacks  │
│   └─────┬─────┘  (чьё время delay истекло)           │
│         │                                            │
│   ┌─────▼──────────┐                                │
│   │  pending I/O   │  отложенные I/O ошибки (TCP)   │
│   └─────┬──────────┘                                │
│         │                                            │
│   ┌─────▼────────┐                                  │
│   │ idle/prepare │  внутреннее (libuv)               │
│   └─────┬────────┘                                  │
│         │                                            │
│   ┌─────▼────┐                                      │
│   │   poll   │  I/O callbacks (fs, net, crypto…)    │
│   │          │  блокирует если очередь пуста         │
│   └─────┬────┘  и нет pending timers/immediates     │
│         │                                            │
│   ┌─────▼────┐                                      │
│   │  check   │  setImmediate callbacks               │
│   └─────┬────┘                                      │
│         │                                            │
│   ┌─────▼──────────────┐                            │
│   │  close callbacks   │  socket.on(‘close’, …)     │
│   └────────────────────┘                            │
│                                                      │
│  ⚡ Между КАЖДОЙ фазой:                              │
│     1) process.nextTick queue (все до пустоты)      │
│     2) Promise microtask queue (все до пустоты)     │
└──────────────────────────────────────────────────────┘
// process.nextTick приоритетнее Promise microtasks
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');
// sync → nextTick → promise
// setImmediate vs setTimeout в main module: порядок НЕ детерминирован
setTimeout(() => console.log('setTimeout'));
setImmediate(() => console.log('setImmediate'));
// Может быть: setTimeout→setImmediate ИЛИ setImmediate→setTimeout
// Зависит от времени запуска loop относительно delay=0
// Внутри I/O callback: setImmediate ВСЕГДА раньше setTimeout
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
// setImmediate → setTimeout (всегда! poll→check фаза)
});
// process.nextTick рекурсия — I/O starve
function dangerousRec() {
process.nextTick(dangerousRec); // никогда не выйдет из nextTick queue
}
// В Node.js есть защита --max-ticks-per-iteration
// Zalgo-safe API: не мешать sync и async колбэки
function readCached(key, cb) {
const cached = cache.get(key);
if (cached) {
// НЕЛЬЗЯ: cb(null, cached) — sync вызов нарушает контракт
process.nextTick(() => cb(null, cached)); // всегда async
return;
}
db.read(key, cb); // async
}
// Смешанное sync/async поведение (Zalgo) ломает порядок событий
// у вызывающего кода — классическая ошибка Node.js API дизайна

Итог: Node.js event loop имеет шесть фаз libuv с чёткой очерёдностью; process.nextTick и Promise microtasks выполняются между фазами, при этом nextTick имеет более высокий приоритет.