Пошук уроків, статей та іншого контенту
Чим Event Loop у Node.js відрізняється від браузерного — фази libuv і черговість виконання коду.
Курс JavaScript уже пояснював Event Loop на прикладі браузера: стек викликів, черга макрозадач, черга мікрозадач. Загальний принцип (JavaScript однопотоковий, асинхронні операції виконуються десь «поза» основним потоком, а результат повертається через чергу задач) той самий у Node.js — але конкретна реалізація відрізняється, бо в Node.js немає браузера, натомість є libuv із власною внутрішньою структурою фаз.
Node.js Event Loop проходить через послідовність фаз по колу, кожна зі своєю чергою колбеків:
timers — виконує колбеки setTimeout і setInterval, час яких настав.
pending callbacks — виконує деякі колбеки відкладені з попереднього циклу (специфічні системні операції).
poll — отримує нові I/O-події (завершені операції з файлами, мережею) і виконує їхні колбеки; тут Event Loop може «затриматись», чекаючи нових подій, якщо черга порожня.
check — виконує колбеки setImmediate.
close callbacks — виконує колбеки закриття (наприклад, socket.on('close', ...)).
Для практичної роботи з Node.js не обов'язково пам'ятати назви всіх п'яти фаз напам'ять — важливіше розуміти сам принцип: Event Loop циклічно обробляє різні категорії відкладених операцій у певному порядку, і мікрозадачі (Promise-колбеки, process.nextTick) виконуються між кожною окремою макрозадачею, а не лише між фазами.
process.nextTick() — специфічна для Node.js функція (аналогів у браузері немає): її колбек виконується одразу після завершення поточної операції, ще до переходу до наступної фази Event Loop і навіть раніше за звичайні Promise-мікрозадачі:
console.log("1: синхронний код");
setTimeout(() => console.log("4: setTimeout (фаза timers)"), 0);
setImmediate(() => console.log("5: setImmediate (фаза check)"));
Promise.resolve().then(() => console.log("3: мікрозадача Promise"));
process.nextTick(() => console.log("2: process.nextTick"));
console.log("1.5: ще синхронний код");
// Порядок виводу: 1, 1.5, 2, 3, 4, 5
// (nextTick і Promise виконуються між макрозадачами, nextTick — з найвищим пріоритетом)У межах основного модуля порядок виконання setTimeout(fn, 0) і setImmediate() не гарантований — залежить від продуктивності системи в конкретний момент. Але всередині колбека I/O-операції (наприклад, після fs.readFile) setImmediate завжди виконається раніше за setTimeout(fn, 0), бо колбек I/O спрацьовує у фазі poll, а наступна фаза за нею — саме check (де живе setImmediate), тоді як timers — на наступному повному колі.
Оскільки весь JavaScript-код у Node.js виконується в одному потоці, довга синхронна операція (важкий цикл, синхронний виклик на кшталт fs.readFileSync для великого файлу) блокує обробку геть усіх інших запитів, поки не завершиться — сервер, що обробляє тисячі запитів на секунду, миттєво «зависає» для всіх користувачів через одну повільну синхронну операцію.
Використовувати синхронні версії функцій (fs.readFileSync, crypto.pbkdf2Sync) у коді, що обробляє HTTP-запити, — блокує Event Loop і затримує відповідь усім іншим користувачам, поки операція не завершиться.
Розраховувати на конкретний порядок setTimeout(fn, 0) проти setImmediate() поза колбеком I/O-операції — цей порядок не гарантований на верхньому рівні модуля.
Зловживати process.nextTick() для рекурсивних відкладених викликів — оскільки він виконується раніше за I/O, це може повністю «заморозити» Event Loop, не даючи дійти до фази poll узагалі (так званий «nextTick starvation»).
Event Loop у Node.js побудований на libuv і проходить фази timers → pending callbacks → poll → check → close по колу, а process.nextTick і Promise-мікрозадачі виконуються між кожною окремою операцією з найвищим пріоритетом. Практичний наслідок для розробника той самий, що й у браузері, лише гостріший: JavaScript-код однопотоковий, і будь-яка довга синхронна операція блокує обробку всіх інших запитів, поки не завершиться.