Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює Event Loop у Node.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Event Loop (цикл подій)** - механізм який дозволяє однопотоковому Node.js обробляти паралельний I/O без блокування. Callbacks проходять через шість впорядкованих фаз (timers, pending, poll, check, close), а між кожним переходом повністю виконуються `process.nextTick` та Promise-мікрозавдання. ```javascript setTimeout(() => console.log('timer'), 0); // фаза timers setImmediate(() => console.log('immediate')); // фаза check Promise.resolve().then(() => console.log('promise')); // мікрозавдання process.nextTick(() => console.log('nextTick')); // мікрозавдання (першим) // Виведення: nextTick -> promise -> timer -> immediate ``` **Ключове:** якщо заблокувати головний потік важкими циклами або `fs.readFileSync`, event loop не зможе обробляти жодні інші callbacks.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Event Loop (цикл подій)** - це механізм, який дозволяє однопотоковому Node.js обробляти паралельні I/O-операції без блокування. Він делегує роботу ОС та libuv, а потім повертає готові callbacks через шість впорядкованих фаз. ## Теорія ### TL;DR - Node.js однопотоковий, але не блокуючий: event loop передає I/O в libuv та ОС і підбирає результати коли вони готові - Аналогія: офіціант, який приймає замовлення і продовжує обслуговувати інші столики, замість того щоб стояти і чекати - Шість фаз за тік у фіксованому порядку: timers, pending callbacks, idle/prepare, poll, check, close - Мікрозавдання (microtasks): `process.nextTick` та Promise-callbacks повністю виконуються між кожним переходом між фазами, причому nextTick першим - Заблокуй головний потік важким CPU-циклом і весь цикл фаз зупиниться ### Швидкий приклад ```javascript const fs = require('fs'); console.log('1: sync'); process.nextTick(() => console.log('2: nextTick')); // мікрозавдання, найвищий пріоритет Promise.resolve().then(() => console.log('3: promise')); // мікрозавдання setTimeout(() => console.log('4: timer'), 0); // фаза timers setImmediate(() => console.log('5: immediate')); // фаза check fs.readFile(__filename, () => { setImmediate(() => console.log('6: I/O immediate')); // check, перед наступними timers setTimeout(() => console.log('7: I/O timer'), 0); }); // Виведення: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 // Всередині I/O callback 6 завжди перед 7 ``` Спочатку виконується синхронний код. Потім мікрозавдання. Потім event loop проходить фази. Всередині I/O callback `setImmediate` завжди спрацьовує перед `setTimeout(fn, 0)`, бо фаза check іде одразу після poll. ### Як event loop працює зсередини V8 виконує JavaScript синхронно в головному потоці. Коли код викликає `fs.readFile` або робить мережевий запит, Node.js передає роботу libuv - C-бібліотеці, що входить до складу Node. libuv використовує асинхронні API ядра (epoll на Linux, kqueue на macOS) для мережевого I/O, а для файлових операцій, DNS і crypto - власний пул потоків (за замовчуванням 4 потоки, змінюється через `UV_THREADPOOL_SIZE`). Коли операція завершується, libuv кладе callback у чергу відповідної фази. Event loop проходить ці черги у фіксованому порядку. Один повний прохід називається тіком (tick). ### Шість фаз тіку ``` ┌───────────────────────────┐ ┌─>│ timers │ <- callbacks setTimeout, setInterval │ └─────────────┬─────────────┘ │ │ <- мікрозавдання (nextTick + Promise) │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ <- I/O callbacks відкладені з попередньої ітерації │ └─────────────┬─────────────┘ │ │ <- мікрозавдання │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ <- тільки внутрішнє використання (libuv) │ └─────────────┬─────────────┘ │ │ <- мікрозавдання │ ┌─────────────┴─────────────┐ │ │ poll │ <- отримання нових I/O-подій, виконання I/O callbacks │ └─────────────┬─────────────┘ │ │ <- мікрозавдання │ ┌─────────────┴─────────────┐ │ │ check │ <- callbacks setImmediate │ └─────────────┬─────────────┘ │ │ <- мікрозавдання │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ <- socket.on('close'), stream.on('close') └───────────────────────────┘ ``` **Timers:** Виконує callbacks чий `setTimeout` або `setInterval` вже спрацював. `setTimeout(fn, 0)` означає "виконай у наступній фазі timers, не раніше ніж через 0мс" - а не "виконай зараз". При навантаженні реальне виконання може затриматися. **Pending callbacks:** I/O callbacks відкладені з попередньої ітерації - наприклад, деякі TCP-помилки від ОС. **Poll:** Серце event loop. Node.js отримує завершені I/O-події від ОС і виконує їх callbacks. Якщо черга порожня і немає зареєстрованих `setImmediate` callbacks, цикл зупиняється тут і чекає нових подій. Він рухається далі коли спрацьовує таймер або з'являється `setImmediate`. **Check:** Виконуються callbacks `setImmediate`. Оскільки ця фаза іде одразу після poll, `setImmediate` всередині I/O callback завжди спрацьовує до фази timers наступного тіку. **Close callbacks:** Фаза очищення. `socket.destroy()` запускає `socket.on('close', ...)` саме тут. ### Пріоритет черги мікрозавдань Між кожним переходом між фазами Node.js зупиняється і спустошує дві черги в такому порядку: 1. Callbacks `process.nextTick` 2. Callbacks Promise `.then()` ```javascript console.log('1: sync'); setTimeout(() => console.log('5: timer'), 0); setImmediate(() => console.log('6: immediate')); Promise.resolve().then(() => console.log('3: promise')); process.nextTick(() => console.log('2: nextTick')); console.log('4: sync end'); // Виведення: // 1: sync // 4: sync end // 2: nextTick <- спочатку черга nextTick // 3: promise <- потім мікрозавдання Promise // 5: timer <- фаза timers (наступний тік) // 6: immediate <- фаза check (наступний тік) ``` `process.nextTick` не належить жодній фазі. Він виконується щойно завершується поточна операція, ще до того як запуститься будь-яка фаза. Це робить його корисним для callbacks, які мають виконатися "після цього синхронного блоку, але до будь-якого I/O". ### Коли що використовувати - HTTP API і сервери: event loop обробляє тисячі паралельних з'єднань в одному потоці - Real-time додатки з WebSockets: фаза check тримає `setImmediate`-емісії швидкими - Важкі CPU-задачі (обробка зображень, криптографія): виноси в `worker_threads`, не в головний потік - Ітеративні фонові задачі: використовуй `setImmediate` замість `process.nextTick`, щоб I/O не блокувався між ітераціями ### Типові помилки **Припущення що `setTimeout(fn, 0)` виконається одразу після поточного коду:** ```javascript setTimeout(() => console.log('timer'), 0); process.nextTick(() => console.log('nextTick')); // Виведення: nextTick, потім timer // nextTick спустошується ще до того як цикл дійде до фази timers ``` **Блокування циклу синхронним CPU-навантаженням:** ```javascript // Заморожує всі інші запити до завершення app.get('/compute', (req, res) => { let sum = 0; for (let i = 0; i < 1e9; i++) sum += i; res.send(String(sum)); }); // Виправлення: виноси в worker_threads const { Worker } = require('worker_threads'); app.get('/compute', (req, res) => { const worker = new Worker('./compute-worker.js'); worker.on('message', (result) => res.send(String(result))); }); ``` **Рекурсивний `process.nextTick` блокує весь I/O:** ```javascript // Небезпечно: все чекає поки рекурсія не завершиться function recurse(n) { if (n === 0) return; process.nextTick(() => recurse(n - 1)); } recurse(1000000); // I/O, таймери, setImmediate - все заблоковано // Безпечно: setImmediate дає циклу дихати між ітераціями function recurse(n) { if (n === 0) return; setImmediate(() => recurse(n - 1)); } ``` **Непередбачуваний порядок `setImmediate` і `setTimeout` поза I/O:** ```javascript // Порядок залежить від системного часу setTimeout(() => console.log('timeout'), 0); setImmediate(() => console.log('immediate')); // Порядок гарантований fs.readFile('/dev/null', () => { setTimeout(() => console.log('timeout'), 0); setImmediate(() => console.log('immediate')); // завжди першим }); ``` Це питання регулярно зустрічається на senior-інтерв'ю. Правило: всередині I/O callback `setImmediate` завжди виграє. Поза ним - залежить від того, чи встиг таймер спрацювати до початку поточного тіку. ### Де це зустрічається в реальному коді - Express.js: callbacks обробників маршрутів виконуються у фазі poll; запити до бази передаються libuv і повертаються в наступній ітерації poll - Socket.io: емісії подій ставляться у чергу фази check, тримаючи затримку низькою - Fastify: ініціалізація плагінів використовує асинхронні фази для запуску без блокування - PM2 cluster mode: запускає один Node-процес на кожне ядро CPU, кожен зі своїм event loop - Node.js streams: події readable/writable приходять через poll, що дозволяє backpressure без блокування ### Follow-up питання **Q:** Які шість фаз event loop і в якому порядку? **A:** Timers, pending callbacks, idle/prepare, poll, check, close. Мікрозавдання (спочатку nextTick, потім Promise) виконуються між кожним переходом. **Q:** Чому `setImmediate` спрацьовує перед `setTimeout(fn, 0)` всередині I/O callback? **A:** Всередині I/O callback цикл знаходиться у фазі poll. Наступна фаза - check, де виконується `setImmediate`. Фаза timers приходить тільки в наступному тіку. **Q:** Що станеться якщо `process.nextTick` рекурсивно викликає сам себе? **A:** Event loop ніколи не просунеться далі. Всі I/O callbacks, таймери і `setImmediate` блокуються до завершення рекурсії. Документація Node.js прямо попереджає про це. **Q:** Що libuv thread pool насправді обробляє? **A:** Файлові операції, DNS-запити і crypto. Мережевий I/O не йде через thread pool - він використовує асинхронні API ядра напряму. За замовчуванням 4 потоки, змінюється через `UV_THREADPOOL_SIZE`. **Q:** Чим event loop в Node відрізняється від браузерного? **A:** Браузери обробляють tasks і microtasks, але не мають явних фаз як check або close. `setImmediate` - тільки Node. Браузери також вставляють рендеринг між батчами задач. **Q:** Senior-питання: ти викликаєш `setImmediate` всередині poll callback. Скільки тіків пройде до його виконання? **A:** Жодного зайвого тіку. Check іде за poll в тому самому тіку. Спочатку виконуються мікрозавдання між фазами, потім callback `setImmediate`. Якщо той callback реєструє ще один `setImmediate`, він виконається в check фазі наступного тіку. ## Приклади ### Базовий: порядок виконання async-операцій ```javascript console.log('A'); setTimeout(() => console.log('D: setTimeout'), 0); Promise.resolve().then(() => console.log('C: Promise')); process.nextTick(() => console.log('B: nextTick')); console.log('E: sync end'); // Виведення: // A // E: sync end // B: nextTick <- мікрозавдання, виконується до будь-якої фази // C: Promise <- мікрозавдання, після черги nextTick // D: setTimeout <- фаза timers, наступний тік ``` Синхронний код завершується першим (A, E). Потім черга nextTick (B). Потім мікрозавдання Promise (C). Лише тоді event loop переходить до фази timers (D). ### setImmediate vs setTimeout всередині і поза I/O ```javascript const fs = require('fs'); // Поза I/O callback - порядок непередбачуваний setTimeout(() => console.log('outer timeout'), 0); setImmediate(() => console.log('outer immediate')); // Будь-який порядок можливий // Всередині I/O callback - порядок фіксований fs.readFile('/dev/null', () => { setTimeout(() => console.log('inner timeout'), 0); setImmediate(() => console.log('inner immediate')); // Завжди: inner immediate -> inner timeout }); ``` Поза I/O callback фаза timers могла вже пройти до реєстрації `setImmediate` - тому порядок залежить від часу. Всередині I/O callback цикл у фазі poll, check іде наступним. `setImmediate` виграє завжди. ### Блокування event loop рекурсивним nextTick ```javascript // Небезпечно: весь I/O, таймери і setImmediate чекають function recursiveNextTick(count) { if (count === 0) return; process.nextTick(() => recursiveNextTick(count - 1)); } recursiveNextTick(1000000); // Безпечно: setImmediate дозволяє циклу обробляти I/O між ітераціями function recursiveImmediate(count) { if (count === 0) return; setImmediate(() => recursiveImmediate(count - 1)); } ``` `process.nextTick` створений для невеликих разових відкладень всередині фази. `setImmediate` - правильний інструмент для фонових ітерацій без блокування сервера. Різниця в продакшені: сервер який залишається чутливим, проти сервера який ставить усі вхідні запити в чергу до кінця рекурсії.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.