Пошук уроків, статей та іншого контенту
Розглянемо, як Node.js виконує JavaScript і координує асинхронні операції без блокування головного потоку.
Event Loop — це механізм Node.js, який дає змогу виконувати асинхронні операції, не блокуючи головний потік JavaScript.
Node.js запускає JavaScript в одному основному потоці. Це означає, що в один момент часу виконується лише одна JavaScript-операція. Проте Node.js може передавати тривалі операції операційній системі або бібліотеці libuv, а потім обробляти їхній результат, коли вони завершаться.
Завдяки цьому програма може:
обробляти HTTP-запити;
читати файли;
працювати з мережею;
виконувати таймери;
очікувати відповідь від бази даних;
і водночас не зупиняти виконання всього застосунку.
Синхронний код виконується послідовно — рядок за рядком.
console.log('Початок');
const result = 2 + 3;
console.log(`Результат: ${result}`);
console.log('Кінець');Результат:
Початок
Результат: 5
КінецьПоки виконується поточна операція, наступний код не починає виконуватися. JavaScript зберігає поточні виклики функцій у спеціальній структурі — стеку викликів.
Наприклад:
function first() {
second();
}
function second() {
console.log('Виконується second');
}
first();Під час виконання:
first() додається до стеку викликів.
Усередині first() викликається second().
second() додається до стеку.
second() завершується і видаляється зі стеку.
first() завершується і видаляється зі стеку.
Поки стек зайнятий довгою синхронною операцією, Node.js не може виконувати інший JavaScript-код.
Асинхронна операція починається зараз, але її результат обробляється пізніше.
Розглянемо таймер:
console.log('Початок');
setTimeout(() => {
console.log('Таймер завершився');
}, 1000);
console.log('Кінець');Результат:
Початок
Кінець
Таймер завершивсяsetTimeout не зупиняє виконання програми на одну секунду. Node.js реєструє таймер і продовжує виконувати наступний код.
Після завершення заданого часу функція з таймера стає готовою до виконання. Event Loop виконає її тоді, коли стек викликів буде вільним.
Важливо: setTimeout(callback, 1000) означає не раніше ніж через 1000 мілісекунд, а не рівно через 1000 мілісекунд. Якщо головний потік зайнятий, callback виконається пізніше.
Спрощено процес виглядає так:
Node.js виконує синхронний код у стеку викликів.
Асинхронні операції передаються операційній системі або libuv.
Коли операція завершується, її callback потрапляє до черги.
Event Loop перевіряє, чи вільний стек викликів.
Якщо стек вільний, callback переноситься у стек і виконується.
Цикл повторюється.
Event Loop не виконує JavaScript паралельно. Він лише координує, коли саме можна виконати наступну готову функцію.
Окрім звичайних callback, Node.js має черги мікрозадач. До них належать:
process.nextTick();
callbacks промісів, зокрема .then(), .catch() і .finally().
Мікрозадачі виконуються після завершення поточного синхронного коду, але до переходу до наступного етапу Event Loop.
У Node.js callback із process.nextTick() зазвичай виконується раніше за callback промісу.
console.log('1. Початок');
setTimeout(() => {
console.log('5. Таймер');
}, 0);
Promise.resolve().then(() => {
console.log('4. Promise');
});
process.nextTick(() => {
console.log('3. nextTick');
});
console.log('2. Кінець');Результат:
1. Початок
2. Кінець
3. nextTick
4. Promise
5. ТаймерПояснення:
Синхронний код виконується негайно.
Код у process.nextTick() виконується після синхронного коду.
Callback промісу виконується після process.nextTick().
Callback таймера виконується пізніше, на відповідному етапі Event Loop.
У спрощеному вигляді Event Loop Node.js має такі етапи:
timers — виконання callback для setTimeout і setInterval;
pending callbacks — виконання деяких системних callback;
poll — очікування та обробка подій введення-виведення;
check — виконання callback для setImmediate;
close callbacks — обробка подій закриття ресурсів.
Для початку достатньо запам’ятати головне:
синхронний код має пріоритет і виконується першим;
мікрозадачі виконуються після поточного синхронного коду;
callback асинхронних операцій виконується тоді, коли Event Loop доходить до відповідної черги;
порядок деяких callback може залежати від того, де саме вони були заплановані.
Одна довга синхронна операція блокує весь головний потік:
console.log('Початок');
const start = Date.now();
while (Date.now() - start < 3000) {
// Навмисно блокуємо головний потік на три секунди
}
console.log('Кінець');Поки виконується цикл:
Node.js не обробляє нові callback;
не виконує інший JavaScript-код;
не відповідає на нові HTTP-запити;
не обробляє готові результати асинхронних операцій.
Навіть якщо асинхронна операція завершилася під час цього циклу, її callback чекатиме, доки стек викликів не звільниться.
Тому для Node.js важливо не виконувати великі блокувальні операції в головному потоці.
async і awaitКлючові слова async і await спрощують роботу з промісами, але не змінюють принцип роботи Event Loop.
function wait(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
async function run() {
console.log('Початок');
await wait(1000);
console.log('Одна секунда минула');
}
run();
console.log('Цей код виконається раніше');Результат:
Початок
Цей код виконається раніше
Одна секунда минулаКоли виконання доходить до await, функція призупиняється до завершення промісу. При цьому головний потік не блокується: Node.js може виконувати інший код.
await не означає «зупинити весь Node.js». Він призупиняє лише виконання поточної async-функції.
Навіть якщо асинхронна операція завершилася дуже швидко, її callback не перериває поточний синхронний код.
console.log('Початок');
Promise.resolve().then(() => {
console.log('Callback промісу');
});
console.log('Кінець');Результат:
Початок
Кінець
Callback промісуСпочатку Node.js завершує поточний синхронний код, а вже потім обробляє мікрозадачу промісу.
Це правило допомагає уникати помилкового очікування, що асинхронний callback виконається одразу після реєстрації.
Для більшості простих ситуацій можна використовувати таку модель:
Виконується весь поточний синхронний код.
Виконуються callbacks process.nextTick().
Виконуються callbacks промісів.
Event Loop переходить до асинхронних callback, наприклад таймерів або операцій введення-виведення.
Після цього цикл повторюється.
Це спрощена модель. Реальний порядок між різними типами асинхронних операцій може залежати від етапу Event Loop і моменту, коли операцію було заплановано.
setTimeout виконається точно у визначений моментsetTimeout(() => {
console.log('Callback');
}, 100);Це означає, що callback може бути виконаний не раніше ніж через 100 мілісекунд. Якщо головний потік зайнятий, виконання відбудеться пізніше.
Довгий цикл або складне синхронне обчислення блокує весь головний потік. Через це асинхронні callback не можуть бути виконані вчасно.
async і await паралельним виконаннямasync і await не запускають JavaScript у новому потоці. Вони лише надають зручний синтаксис для роботи з промісами та дозволяють не блокувати Event Loop під час очікування асинхронної операції.
Порядок залежить від типу операції:
синхронний код виконується першим;
мікрозадачі мають пріоритет після синхронного коду;
таймери та інші callback обробляються на етапах Event Loop.
Node.js виконує JavaScript у головному потоці.
Event Loop координує асинхронні операції та їхні callback.
Асинхронні операції не блокують виконання синхронного коду.
Callback виконується лише тоді, коли стек викликів вільний.
Мікрозадачі, зокрема process.nextTick() і callbacks промісів, мають високий пріоритет.
setTimeout задає мінімальну затримку, а не точний момент виконання.
Довгий синхронний код може заблокувати весь Node.js-застосунок.
async і await призупиняють лише поточну async-функцію, а не весь Event Loop.