Event Loop в Node.js для начинающих

Backend2026-09-14TryQuickToolBox

Вы наверняка слышали, что Node.js однопоточный и использует event loop для обработки тысяч одновременных соединений. Но что это на самом деле значит? Если вы новичок в Node.js, event loop может показаться загадочным чёрным ящиком. Эта статья объясняет его простым языком, чтобы вы писали более качественный и быстрый код на Node.js.

Зачем нужен event loop

Традиционные веб-серверы (например, Apache с PHP) создают новый поток или процесс для каждого запроса. Это работает, но плохо масштабируется при большом количестве одновременных соединений — каждый поток потребляет память и процессорное время. Node.js использует другой подход: он выполняет ваш JavaScript в одном потоке и использует event loop для асинхронной обработки операций ввода-вывода. Это значит, что ваш код не ждёт завершения запроса к базе данных или чтения файла; он продолжает выполнять другие задачи и возвращается, когда результат готов.

Основные компоненты

Прежде чем углубляться в event loop, разберём ключевых участников:

Как работает event loop: простой пример

Рассмотрим такой код:

console.log('Start');

setTimeout(() => {
  console.log('Timeout');
}, 0);

console.log('End');

Какой будет вывод? Если вы предположили Start, End, Timeout, вы правы. Хотя таймаут равен 0 миллисекунд, колбэк не выполняется немедленно. Вот что происходит:

  1. console.log('Start') помещается в стек вызовов, выполняется и удаляется из него.
  2. Вызывается setTimeout. Node.js регистрирует таймер и устанавливает его срабатывание через 0 мс. Колбэк сохраняется, и функция возвращает управление.
  3. console.log('End') помещается в стек, выполняется и удаляется.
  4. Теперь стек вызовов пуст. 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 обеспечивает неблокирующий ввод-вывод:

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, чтобы быстро разбирать и визуализировать данные логов.