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 microtasksPromise.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 ВСЕГДА раньше setTimeoutconst fs = require('fs');fs.readFile(__filename, () => { setTimeout(() => console.log('setTimeout'), 0); setImmediate(() => console.log('setImmediate')); // setImmediate → setTimeout (всегда! poll→check фаза)});
// process.nextTick рекурсия — I/O starvefunction 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 имеет более высокий приоритет.