Пошук уроків, статей та іншого контенту
Розберемо фази Event Loop: Timers, I/O та Poll і визначимо порядок виконання обробників.
Node.js виконує JavaScript в одному основному потоці. Замість блокування під час очікування файлової операції або мережевої відповіді Node.js передає таку роботу операційній системі, а після завершення ставить callback у чергу.
Event Loop постійно перевіряє ці черги та запускає готові callback-и у визначеному порядку.
Спрощено важливі для цієї теми фази виглядають так:
Timers — запускаються callback-и таймерів.
Poll — обробляються завершені операції введення-виведення.
Check — запускаються callback-и setImmediate().
Між ними є й інші фази, але для розуміння setTimeout(), I/O та setImmediate() достатньо зосередитися на цих трьох.
У фазі Timers Node.js перевіряє таймери, створені за допомогою:
setTimeout();
setInterval().
Наприклад:
setTimeout(() => {
console.log('Таймер завершився');
}, 1000);
console.log('Синхронний код');Результат:
Синхронний код
Таймер завершивсяЗначення 1000 у цьому прикладі означає мінімальну затримку, а не точний момент виконання.
setTimeout(callback, 1000);Це означає:
Запусти
callbackне раніше ніж через 1000 мс, коли Event Loop отримає можливість це зробити.
Якщо основний потік зайнятий, callback таймера чекатиме довше.
setTimeout(() => {
console.log('Таймер');
}, 0);
console.log('Після встановлення таймера');Результат:
Після встановлення таймера
ТаймерНавіть setTimeout(..., 0) не запускає callback негайно. Спочатку має завершитися весь поточний синхронний код, а потім Event Loop зможе обробити таймер.
Фаза Poll відповідає за операції введення-виведення, або I/O:
читання файлів;
роботу з мережевими з'єднаннями;
отримання даних від сокетів;
інші асинхронні операції Node.js.
Коли операція I/O завершується, її callback потрапляє до черги Poll. Event Loop виконує цей callback під час фази Poll.
Приклад читання файлу:
const fs = require('node:fs');
fs.readFile(__filename, 'utf8', (error, data) => {
if (error) {
console.error('Помилка читання:', error.message);
return;
}
console.log(`Файл прочитано: ${data.length} символів`);
});
console.log('Запит на читання файлу надіслано');Спочатку буде виведено:
Запит на читання файлу надісланоCallback fs.readFile() буде виконано пізніше, коли читання файлу завершиться та Event Loop обробить відповідний I/O callback у фазі Poll.
Якщо в Poll немає готових I/O callback-ів, Event Loop може тимчасово чекати на нові події.
Наприклад, сервер може перебувати в Poll і чекати на нове мережеве з'єднання. Це очікування не блокує весь Node.js так, як блокувала б синхронна операція JavaScript.
Коли з'являється готова I/O-операція, її callback виконується у фазі Poll.
setImmediate() і фаза ChecksetImmediate() ставить callback у чергу фази Check.
setImmediate(() => {
console.log('setImmediate');
});Фаза Check виконується після Poll. Тому setImmediate() особливо корисний, коли потрібно виконати код одразу після обробки поточних I/O callback-ів.
Якщо setTimeout() і setImmediate() створені всередині I/O callback, setImmediate() зазвичай виконується першим.
const fs = require('node:fs');
fs.readFile(__filename, 'utf8', (error) => {
if (error) {
console.error(error.message);
return;
}
console.log('I/O завершено');
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});Типовий результат:
I/O завершено
setImmediate
setTimeoutПорядок пояснюється фазами:
fs.readFile() завершується.
Його callback виконується у фазі Poll.
setImmediate() планує callback для фази Check.
setTimeout() планує callback для наступної фази Timers.
Після Poll Event Loop переходить до Check, тому setImmediate() виконується першим.
Такий порядок є надійним для цього сценарію: обидва callback-и створені всередині I/O callback.
setTimeout(0) проти setImmediate() на верхньому рівніЯкщо створити setTimeout(0) і setImmediate() у головному модулі, порядок не варто вважати гарантованим:
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});Можливий один результат:
setTimeout
setImmediateА можливо й так:
setImmediate
setTimeoutПричина в тому, що порядок залежить від моменту запуску Event Loop, операційної системи та інших факторів.
Тому не слід будувати логіку програми на припущенні, що setTimeout(..., 0) завжди виконається раніше за setImmediate() або навпаки.
Розглянемо приклад, який поєднує синхронний код, I/O, таймер і setImmediate():
const fs = require('node:fs');
console.log('1. Початок');
fs.readFile(__filename, 'utf8', (error) => {
if (error) {
console.error('Помилка:', error.message);
return;
}
console.log('3. I/O callback');
setTimeout(() => {
console.log('5. Таймер');
}, 0);
setImmediate(() => {
console.log('4. setImmediate');
});
});
console.log('2. Кінець синхронного коду');Очікуваний результат:
1. Початок
2. Кінець синхронного коду
3. I/O callback
4. setImmediate
5. ТаймерПояснення:
рядки 1 і 2 виконуються одразу синхронно;
callback fs.readFile() виконується після завершення читання;
усередині I/O callback створюються setTimeout() і setImmediate();
фаза Check настає раніше за наступну обробку таймера, тому setImmediate() виконується першим.
Event Loop не може виконати інший JavaScript-код, поки поточний синхронний код не завершиться.
console.log('Початок');
setTimeout(() => {
console.log('Таймер');
}, 0);
const end = Date.now() + 2000;
while (Date.now() < end) {
// Навмисно блокуємо основний потік на дві секунди
}
console.log('Кінець');Результат буде приблизно таким:
Початок
Кінець
ТаймерТаймер мав затримку 0, але його callback не міг виконатися під час циклу while. Event Loop отримав можливість обробити таймер лише після завершення синхронного коду.
Тому асинхронність Node.js не означає, що JavaScript-код виконується паралельно в тому самому потоці.
setTimeout(callback, 100);Це не гарантує запуск рівно через 100 мс. Callback може бути виконаний пізніше, якщо Event Loop зайнятий.
setTimeout(..., 0) миттєвимНульова затримка лише дозволяє поставити callback у чергу таймерів. Callback не перериває поточний синхронний код.
setTimeout() і setImmediate()На верхньому рівні модуля їхній порядок може відрізнятися. Надійний порядок setImmediate() перед setTimeout(0) характерний для випадку, коли обидва виклики створені всередині I/O callback.
Операція читання або мережевий запит можуть завершитися раніше, але їхній callback буде виконано лише тоді, коли Event Loop обробить відповідну чергу.
Великі цикли, синхронне читання файлів або складні обчислення затримують таймери та I/O callback-и.
Timers обробляє callback-и setTimeout() і setInterval().
Затримка таймера є мінімальною, а не точною.
Poll обробляє готові callback-и операцій введення-виведення.
Якщо I/O ще не завершено, Event Loop може чекати в Poll.
setImmediate() виконується у фазі Check, після Poll.
Усередині I/O callback setImmediate() зазвичай виконується раніше за setTimeout(..., 0).
На верхньому рівні порядок між setTimeout(..., 0) і setImmediate() не гарантований.
Довгий синхронний код блокує Event Loop і затримує всі асинхронні callback-и.