Event Loop в Node.js для начинающих
Вы наверняка слышали, что Node.js однопоточный и использует event loop для обработки тысяч одновременных соединений. Но что это на самом деле значит? Если вы новичок в Node.js, event loop может показаться загадочным чёрным ящиком. Эта статья объясняет его простым языком, чтобы вы писали более качественный и быстрый код на Node.js.
Зачем нужен event loop
Традиционные веб-серверы (например, Apache с PHP) создают новый поток или процесс для каждого запроса. Это работает, но плохо масштабируется при большом количестве одновременных соединений — каждый поток потребляет память и процессорное время. Node.js использует другой подход: он выполняет ваш JavaScript в одном потоке и использует event loop для асинхронной обработки операций ввода-вывода. Это значит, что ваш код не ждёт завершения запроса к базе данных или чтения файла; он продолжает выполнять другие задачи и возвращается, когда результат готов.
Основные компоненты
Прежде чем углубляться в event loop, разберём ключевых участников:
- Стек вызовов (Call Stack): Здесь выполняется ваш синхронный JavaScript-код. Функции добавляются в стек и удаляются по мере выполнения.
- Node API: C++ API, которые обрабатывают ресурсоёмкие операции, такие как файловый ввод-вывод, сетевые запросы и таймеры. Они работают в фоне (используя пул потоков libuv).
- Очередь колбэков (Callback Queue): Здесь ожидают выполнения колбэки от завершённых операций Node API.
- Event Loop: Оркестратор, который постоянно проверяет, пуст ли стек вызовов, и если да, перемещает колбэки из очереди в стек.
Как работает event loop: простой пример
Рассмотрим такой код:
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
Какой будет вывод? Если вы предположили Start, End, Timeout, вы правы. Хотя таймаут равен 0 миллисекунд, колбэк не выполняется немедленно. Вот что происходит:
console.log('Start')помещается в стек вызовов, выполняется и удаляется из него.- Вызывается
setTimeout. Node.js регистрирует таймер и устанавливает его срабатывание через 0 мс. Колбэк сохраняется, и функция возвращает управление. console.log('End')помещается в стек, выполняется и удаляется.- Теперь стек вызовов пуст. Event loop проверяет фазу таймеров и видит, что таймер истёк. Он перемещает колбэк в стек вызовов, который выводит
Timeout.
Это демонстрирует неблокирующую природу: таймер не блокирует остальной код.
Фазы event loop
Event loop обрабатывает колбэки в несколько фаз, каждая из которых имеет своё назначение. Понимание этих фаз помогает предсказывать порядок выполнения.
| Фаза | Описание |
|---|---|
| Timers | Выполняет колбэки, запланированные через setTimeout() и setInterval(). |
| Pending Callbacks | Выполняет колбэки ввода-вывода, отложенные до следующей итерации цикла. |
| Idle, Prepare | Используется внутренне в Node.js. |
| Poll | Извлекает новые события ввода-вывода; выполняет связанные с вводом-выводом колбэки (почти все, кроме таймеров, setImmediate и close-колбэков). |
| Check | Выполняет колбэки, запланированные через setImmediate(). |
| Close Callbacks | Выполняет close-колбэки, например, socket.on('close', ...). |
Между каждой фазой Node.js проверяет микрозадачи: колбэки process.nextTick() и промисы. Они имеют более высокий приоритет и выполняются сразу после завершения текущей операции, до перехода к следующей фазе.
Микрозадачи: nextTick и промисы
Микрозадачи не являются частью фаз event loop; они обрабатываются после каждой фазы и после каждого колбэка. Это заставляет их выполняться раньше таймеров и колбэков ввода-вывода. Например:
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('Sync');
Вывод: Sync, nextTick, Promise, Timeout. Синхронный код выполняется первым, затем микрозадачи (nextTick перед промисами), затем таймер.
Предупреждение: Рекурсивные вызовы process.nextTick() могут «заморить» event loop, препятствуя выполнению ввода-вывода. Используйте setImmediate() для рекурсивных операций.
setImmediate против setTimeout
setImmediate() предназначен для выполнения колбэка сразу после завершения текущей фазы poll. В отличие от него, setTimeout() с 0 мс ждёт следующей фазы таймеров. Порядок между ними может варьироваться в зависимости от контекста, но внутри колбэка ввода-вывода setImmediate() всегда выполняется первым.
Почему это важно для вашего кода
Понимание event loop помогает избежать распространённых ошибок:
- Не блокируйте event loop: Синхронные операции, такие как
fs.readFileSyncили тяжёлые вычисления, блокируют весь цикл, делая ваше приложение неотзывчивым. Используйте асинхронные версии или выносите вычисления в worker threads. - Разумно используйте микрозадачи: Они выполняются перед вводом-выводом, поэтому тяжёлая обработка микрозадач может задержать колбэки ввода-вывода.
- Понимайте порядок колбэков: При смешивании таймеров, промисов и ввода-вывода знайте фазы, чтобы предсказать вывод.
Практический пример: неблокирующее чтение файла
Вот как event loop обеспечивает неблокирующий ввод-вывод:
const fs = require('fs');
console.log('Before read');
fs.readFile('large-file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log('File read complete');
});
console.log('After read');
Вывод: Before read, After read, затем File read complete. Чтение файла происходит в фоне, а колбэк ставится в очередь по завершении. Тем временем может выполняться другой код.
Часто задаваемые вопросы
Действительно ли Node.js однопоточный?
Node.js выполняет ваш JavaScript в одном потоке, но использует несколько потоков в пуле потоков libuv для файлового ввода-вывода, DNS и других операций. Так что под капотом он не полностью однопоточный.
В чём разница между стеком вызовов и event loop?
Стек вызовов выполняет синхронный код. Event loop управляет асинхронными колбэками, перемещая их из очередей в стек вызовов, когда он пуст.
Можно ли создать несколько event loop?
Нет, каждый процесс Node.js имеет один event loop. Однако можно использовать worker threads для запуска отдельных потоков JavaScript, каждый со своим event loop.
Если вы работаете с серверными логами для отладки проблем производительности, попробуйте наш Nginx Log Analyzer, чтобы быстро разбирать и визуализировать данные логов.