Що таке цикл подій, як він працює і чому про нього всі завжди питають на співбесідах? Певний час я не міг чітко відповісти на це питання, а вже потім, коли з часом набрався досвіду і сам почав наймати людей, то стало зрозуміло, що це реально велика прогалина у більшості JavaScript-розробників.
1Що таке Event Loop в Node.js
Event Loop — це цикл, за допомогою якого Node.js має змогу виконувати неблокуючі операції I/O — input/output.
І ще є дуже важливим, що Event Loop — це тільки «серце» великого механізму відомої бібліотеки libuv. Все, що потрібно знати про libuv, я описав у своїй статті «Фундамент для JavaScript-розробника: як відповісти, що таке libuv на співбесіді з Node.js».
Тож зараз ми розглянемо, що всередині Event Loop:
Фази — те, про що далеко не всі знають або говорять, але знання фаз і є фундаментом для розуміння послідовності виконання коду, написаного на JavaScript.
2 Розкажіть про фази
- Timers: фаза, в якій виконуються колбеки, заплановані
setTimeout()
іsetInterval()
. - Pending callbacks: виконує I/O-колбеки, які були відкладені до наступної ітерації циклу.
- Idle, prepare: використовувати тільки внутрішньо.
- Poll: отримання нових подій I/O; виконувати колбеки, пов’язані з I/O (майже всі, за винятком колбеків, які виконуються в фазі
close callbacks
, запланованих таймерами таsetImmediate()
); при необхідності node може тут блокуватися. - Check: тут викликаються
setImmediate() callbacks
. - Сlose callbacks: закриває колбеки такі як
socket/http/eventEmitter/.on(‘close', () =>)
.
З повною інформацією про фази можна ознайомитися тут.
3Що таке мікро- та макрозадачі
Тож після запитань «що таке Event Loop» і «що ви знаєте про фази» запитують, чи знаєте ви, що таке мікрозадачі та макрозадачі, у відповідь на це запитання у мене також є стаття.
А зараз ми розберемо наступне:
- різницю між
setTimeout()
isetImmediate()
; - різницю між
process.nextTick()
іsetImmediate()
; - які труднощі можливо вирішити за допомогою
process.nextTick()
.
4У чому різниця між setTimeout() i setImmediate()
setTimeout()
— колбек, який ми передаємо в таймер, виконується після певного пройденого часу, переданого другим аргументом в setTimeout()
або при відсутності вказаного часу, за замовчуванням через 4 мс.
setImmediate()
виконується після поточної poll-фази:
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('Timeout'); }, 0); setImmediate(() => { console.log('Immediate'); }); });
Результат:
Immediate Timeout
Але що є важливим, що залежно від контексту, в якому знаходяться функції, впливає на те, чій колбек буде виконаний першим.
Коли ми використовуємо обидва таймера в логіці, яка працює з I/O, як в прикладі зверху, то setImmediate()
буде завжди першим при умові, що передані в них колбеки будуть схожі за логікою або однакові.
setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
В іншому випадку таймери будуть виконуватись непередбачувано. Нижче результат двух запусків таймерів:
Immediate Timeout
Timeout Immediate
5У чому різниця між process.nextTick() і setImmediate()
В цій ситуації завжди першим виконується process.nextTick()
, він виконується при наступному тіку (tick) ядра вашого комп’ютера, а це 100-1000 тіків за секунду — і в цьому його небезпека.
Якщо ви написали рекурсивну функцію і там є process.nextTick()
, то поточний цикл Event Loop так може і не завершитись, про це є застереження в офіційній документації.
6Які труднощі можливо вирішити за допомогою process.nextTick()
Є такі ситуації, коли нам треба виконати нашу функцію з мінімальною затримкою лише після того, як код буде ініціалізовано, але ще жодна I/O-операція ще не буде виконана, і тут process.nextTick()
допоможе нам з легкістю:
const EventEmitter = require('events'); class MyEmitter extends EventEmitter { constructor() { super(); process.nextTick(() => { this.emit('event'); }); } } const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); });
Цей приклад коду з офіційної документації, і дуже простий, але він зрозуміло і коротко показує нам яку важливу проблему ми можемо вирішити:
const EventEmitter = require('events'); class MyEmitter extends EventEmitter { constructor() { super(); this.emit('event'); } } const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); });
Якщо виключити process.nextTick()
з нашого прикладу, то подія event
ніколи не спрацює, тому що виклик події відбувся ще у конструкторі класу MyEmitter
, а реєстрація події — пізніше.
Висновок
В мене з досвіду були кандидати з інших країн, які на запитання «що таке Event Loop» відповідали так: «Навіщо ви ставите мені таке елементарне запитання, я людина з досвідом 5+ років, і клієнт за такі знання не платить, він платить за фічі».
Звичайно, всі платять за функціонал, тільки ось цікаво, скільки буде витрачено часу і яка буде якість коду, якщо ти не знаєш, як все працює та яка сила є в наших руках.
Знання таких речей як фази і робота з ними допомагає нам розуміти більш проблемні місця в коді, писати код більш осмислено та вирішувати більш нетривіальні задачі без «милиць», що впливає на надійність і впевненість в роботі нашої системи.
Дякую вам за увагу і продуктивного кодування 😉
Цей матеріал – не редакційний, це – особиста думка його автора. Редакція може не поділяти цю думку.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: