
После прочтения этой статьи вы хорошо поймете следующее:
- как работает цикл событий (event loop) в Node.js, включая все его фазы
- как Node.js выполняет JavaScript в одном потоке (single thread)
- как Node.js использует библиотеку libuv и внутренние API для обработки асинхронных операций
- почему знание работы цикла событий так важно для написания эффективного серверного кода
Понять, как JS работает "под капотом" в браузере (см. эту короткую 15-минутную статью), проще, чем понять устройство Node.js. В браузере нет сложных фаз и многослойных механизмов, как в Node.js.
Но почему в Node.js все устроено сложнее? Зачем столько фаз, которые так непросто понять?
Дело в том, что в браузере JS решает более простые задачи — обработка действий пользователя, таймеров и промисов. Для этого достаточно использовать обычную очередь задач (task queue) и очередь микрозадач (microtask queue), которые работают в связке с Web API.
Node.js — это не просто JS, а целая среда выполнения, предназначенная для работы с JS в любом месте и в любых условиях! В отличие от браузеров, здесь нет Web API, которые могли бы помочь. Эта среда должна работать на наших серверах, поэтому она также работает с файлами, сетевыми запросами и низкоуровневыми системными вызовами (syscalls). Из-за этого Node.js необходим более сложный цикл событий с несколькими фазами, позволяющий правильно обрабатывать все типы операций.
Node.js должен обеспечивать работу JS в любых условиях и при этом с высокой производительностью. Поэтому, помимо задач, которые обычно выполняются в браузерах, ему нужно обрабатывать дополнительные серверные события, которыми важно правильно управлять.
В то время как в браузерах такие вещи, как запросы fetch
, передаются в Web API, в Node.js все немного по-другому. В Node.js эквивалентом концепции "Web API" является библиотека на C/C++ под названием "libuv". Они не идентичны, но можно считать, что обе позволяют сделать JS асинхронным.
В этой статье я часто буду использовать термин "I/O". I/O означает ввод-вывод (Input-Output), и это относится к любым операциям, при которых данные поступают в систему или выходят из нее — например, чтение файла, отправка запроса или получение ответа.
Web API в браузерах:
- это интерфейс, предоставляемый самим браузером
- реализован разработчиками браузеров (Chrome, Firefox, Safari и др.) с использованием таких языков, как C++
- каждый браузер имеет свою собственную реализацию Web API
- работает в рамках браузера, используя его ресурсы
libuv в Node.js:
- это библиотека на C, используемая Node.js для управления асинхронными операциями I/O
- напрямую использует API операционных систем
- абстрагирует специфичные механизмы операционных систем (IOCP на Windows, epoll на Linux и др.)
- является независимым компонентом, который поставляется вместе с Node.js
Основное отличие: в то время как Web API работает в пределах браузера, libuv напрямую обращается к ресурсам операционной системы. Оба управляют асинхронными операциями, но действуют в разных средах.
Обработка запросов:
- Web API: запросы ожидают в подсистемах браузера
- Node.js: запросы ожидают в очередях I/O операционной системы
В обоих случаях основной поток JS не блокируется, но запросы обрабатываются на разных уровнях. В браузере процесс управляется его собственными ресурсами и процессами, а в Node.js — более прямым способом, с использованием механизмов операционной системы.
И снова, в обоих случаях операционная система является основным механизмом, который отслеживает и уведомляет о завершении операций I/O. Операционная система обнаруживает сетевые ответы, операции с файлами и другие события I/O с помощью аппаратных прерываний (hardware interrupts) и системных вызовов.
Теперь у вас есть общее представление о том, что Node.js работает иначе, чем браузер. Если что-то пока не совсем ясно — не переживайте, мы разберем все поэтапно, начиная с самых основ.
❯ Выполнение кода в Node.js
Среда выполнения Node.js — это программа на C++, которая принимает на вход JS-файл, читает его и интерпретирует. Код JS выполняется построчно, поочередно, пока не завершится.
Но в реальности все гораздо сложнее. JS-код не просто читается и выполняется построчно. Бывают ситуации, которые требуют особого подхода. Например, мы можем захотеть выполнить часть кода только после того, как пройдет какое-то время (например, X мс). Пока это время не истечет, нужно подождать. Или, допустим, мы читаем файл из внешнего источника — код должен дождаться окончания этого процесса, прежде чем продолжить выполнение. Или другой пример — наш код слушает входящие подключения на определенном порту. Пользователь может подключиться в любой момент, и мы не знаем, когда он отправит запрос. Но как только это произойдет, нужно сразу среагировать.
В таких случаях простое последовательное выполнение строк кода уже не подходит.
Поэтому нам нужно что-то вроде цикла, который будет проверять, происходят ли какие-то события. Когда они происходят, код должен немедленно выполняться.
Вот почему существует так называемый цикл событий (event loop).
Прежде чем углубляться в цикл событий, нужно понять еще один термин — callback. Функции обратного вызова — это функции, которые вызываются при наступлении определенного события. Например, когда устанавливается таймер, вместе с ним указывается обработчик, который будет выполнен после завершения отсчета. Или, скажем, нужно прочитать файл, и после того как чтение завершится, нужно вызвать определенную функцию. Название callback буквально означает "обратный вызов" — функция "вызывается" в тот момент, когда событие произошло.
Цикл событий — это цикл с разными фазами. Каждая фаза имеет очередь функций обратного вызова, и код завершится, когда в этих очередях не останется функций. Пока не беспокойтесь о деталях — мы подробно разберем каждый этап цикла событий, и все станет ясно.
Цикл событий (упрощенно)
Цикл событий — это механизм, который позволяет Node.js выполнять неблокирующие операции I/O, несмотря на то, что JS изначально однопоточный. Это достигается за счет передачи сложных операций ядру операционной системы, когда это возможно.
При запуске основного кода он выполняется построчно. Если встречается асинхронная операция (например, чтение файла, сетевое соединение или таймер), Node.js просто регистрирует соответствующий обработчик, но не выполняет его сразу. Этот обработчик будет вызван позже, когда соответствующее событие произойдет. Причем обработчики могут регистрировать другие обработчики, создавая цепочку зависимостей. Например, мы можем попросить прочитать файл через 5 секунд, а когда обработчик выполнится, он может запланировать еще один обработчик. Поэтому Node.js продолжает выполнять код до тех пор, пока очереди обработчиков не опустеют.
Процесс выглядит так: выполнение -> проверка очереди обработчиков -> выполнение -> проверка очереди обработчиков… и т.д., пока не останется незавершенных обработчиков.
Важно помнить, что цикл событий начинается только после завершения выполнения начального кода. Выполнение этого начального кода тоже можно считать отдельной фазой.
Теперь давайте разберемся, какие именно фазы проходит код и что происходит на каждом этапе.
Основной модуль
Сначала выполняется код основного модуля (main module). На этом этапе еще нет цикла событий — это начальная фаза.
Начальная фаза выполняется только один раз в самом начале работы программы.
В основном модуле весь код выполняется синхронно. Обработчики еще не выполняются, а сам цикл событий еще не запущен. Мы можем зарегистрировать обработчики, но они не будут выполнены на этом этапе. Основной модуль = начальная фаза.
const x = 1;
const y = x + 1;
setTimeout(() => console.log("Должно выполниться через 1 мс"), 1);
for (let i = 0; i < 10000000; i++);
console.log("Когда это сообщение будет выведено?");
Как вы думаете, какой будет результат? Если вы внимательно читали статью, то, возможно, уже догадались.
Когда это сообщение будет выведено?
Должно выполниться через 1 мс
Это основной модуль, и на этом этапе обработчики не выполняются. Это начальная фаза и начальное выполнение кода.
Чаще всего (при использовании require
) другие модули загружаются еще до начала выполнения начальной фазы. Если эти модули зависят от других, их загрузка будет ждать загрузки всех необходимых субмодулей.
Очень важно, чтобы начальная фаза была как можно короче. Чем быстрее начнется выполнение кода, тем лучше производительность. Поэтому использовать слишком много модулей в коде — не лучшая практика.
Технически, поскольку цикл событий еще не стартовал на начальной фазе (основном модуле), можно не считать ее "фазой". Но теперь цикл заработает! И первая фаза цикла — это фаза таймеров. С ее началом мы переходим к циклу событий, о котором говорили ранее. Теперь рассмотрим каждый шаг, и это будут следующие этапы:

Каждый из этих блоков мы будем называть "фазой" цикла событий.
Каждая фаза имеет свою очередь обработчиков в порядке FIFO (first in — first out — первым вошел — первым вышел). Когда цикл событий входит в очередную фазу, он выполняет операции, связанные с этой фазой, и затем выполняет все обработчики из очереди до тех пор, пока она не станет пустой или не будет достигнут лимит на количество выполняемых обработчиков. Когда очередь пуста или лимит достигнут, цикл событий переходит к следующей фазе.
Такой многократный подход позволяет эффективно приоритизировать различные типы асинхронных операций. Это особенно важно для серверных приложений, которые обрабатывают множество операций I/O, таких как доступ к файловой системе, сетевые запросы и запросы к базам данных. В то время как более простая модель браузера подходит для взаимодействия с пользовательским интерфейсом, модель Node.js с ее фазами оптимизирована для серверных задач, где различные типы операций I/O требуют разного приоритета обработки.
Перед тем как подробно разбирать каждую фазу, стоит взглянуть на общий обзор фаз цикла событий, чтобы понять, как все устроено в целом.
Обзор фаз
Цикл событий в Node.js постоянно проходит через шесть основных фаз в строго определенном порядке. Этот непрерывный процесс обеспечивает корректное выполнение всех видов асинхронных операций: на каждой фазе цикл событий проверяет наличие ожидающих задач, обрабатывает их и только затем переходит к следующей фазе, возвращаясь к началу после завершения полного цикла.
Последовательность этих фаз играет ключевую роль. Это не просто случайно выбранные этапы — каждая фаза имеет свое четко определенное место по вполне конкретным причинам. В процессе изучения вы поймете, почему одни фазы идут раньше, а другие — позже.
-
Timers (таймеры): на этом этапе выполняются функции обратного вызова, запланированные с помощью
setTimeout()
иsetInterval()
. - Pending Callbacks (отложенные функции обратного вызова): на этой фазе Node.js обрабатывает "оставшиеся" с предыдущего цикла функции обратного вызова, в основном связанные с системными операциями, такими как обработка ошибок TCP.
- Idle, Prepare (простой, подготовка): внутренние фазы, используемые Node.js для выполнения собственных служебных задач. Фаза idle может использоваться для очистки, когда в цикле событий временно нет других активных задач, а prepare готовит Node.js к следующей фазе poll, настраивая все необходимое перед проверкой новых событий I/O.
-
Poll (опрос): вероятно, самая важная фаза. Здесь Node.js:
- Проверяет новые события I/O (например, входящие сетевые подключения или файловые операции).
- Выполняет функции обратного вызова для готовых событий I/O.
- При необходимости может временно приостановить выполнение (заблокироваться), чтобы дождаться новых событий.
Эта фаза является одной из самых длительных, так как именно здесь Node.js активно ожидает и обрабатывает внешние события, такие как сетевые запросы, операции с файлами и др.
-
Check (проверка): на этом этапе выполняются обработчики, запланированные с помощью
setImmediate()
. Эта функция позволяет запустить код сразу после завершения фазы poll, но до начала следующего цикла. Это удобно, когда нужно выполнить задачу после всех завершенных операций I/O, но перед запуском новых таймеров. -
Close Callbacks (функции обратного вызова закрытия): эта завершающая фаза обрабатывает функции обратного вызова, связанные с закрытием ресурсов, таких как сокеты (sockets) или файловые дескрипторы. Например, когда сетевое соединение закрывается, обработчик события
close
сокета будет выполнен на этом этапе.
Не волнуйтесь, если пока не все понятно. В дальнейшем мы подробно разберем каждую фазу. А когда дочитаете статью до конца, обязательно вернитесь к этому разделу — вы будете приятно удивлены, сколько нового успели понять.
❯ Подробное описание фаз
Timers

Фаза таймеров начинается сразу после начальной фазы. Именно здесь цикл событий фактически инициализируется. Эту фазу обрабатывает встроенная библиотека libuv.
На этой фазе обработчики таймеров планируются и сортируются по заданной задержке.
Однако важно понимать, что точность этих таймеров не всегда идеальна. Они могут быть задержаны операционной системой или другими фазами цикла. Например, если в начальной фазе выполняется тяжелая операция, таймеру придется дождаться ее завершения, даже если время ожидания было установлено всего в 10 мс. На практике такой таймер может сработать через секунду или даже позже. Поэтому говорят, что таймеры в Node.js не гарантируют точное время выполнения.
Давайте посмотрим на простой пример, чтобы понять, как это работает. Начнем с базовой функции setTimeout
:
setTimeout(timerCallback, 100, "100 ms", 100);
Когда мы используем функцию setTimeout
в Node.js, первым аргументом передается функция, которая будет выполнена после указанного времени задержки (второй аргумент, в мс). Третий и последующие аргументы передаются в функцию timerCallback
в качестве параметров:
const timerCallback = (a, b) =>
console.log(`Timer ${a} delayed for ${Date.now() - start - b}`);
const start = Date.now();
setTimeout(timerCallback, 500, "500 ms", 500);
setTimeout(timerCallback, 0, "0 ms", 0);
setTimeout(timerCallback, 1, "1 ms", 1);
setTimeout(timerCallback, 1000, "1000 ms", 1000);
for (let i = 0; i <= 1000000000; i++);
Аргументы "a" и "b" будут соответствовать третьему и четвертому параметрам функции setTimeout
.
В этом примере мы пытаемся узнать, насколько сильно задержались наши таймеры. Сначала мы сохраняем текущее время в переменную start
, чтобы зафиксировать момент начала работы начальной фазы. Затем внутри функции timerCallback
мы используем Date.now()
, чтобы узнать точное время, когда функция была выполнена.
Логика простая: если мы вычтем start
из значения Date.now()
в момент выполнения функции, то должны получить 500 мс
для первого таймера, 0 мс
для второго, 1 мс
для третьего и 1000 мс
для четвертого, так как они должны сработать через заданное количество мс (второй аргумент setTimeout
). То есть: время выполнения функции — время начала = время, прошедшее до выполнения функции.
При этом значение b показывает задержку в мс, выражение Date.now() - start - b
в идеале должно быть равно нулю.
Но это только в теории. На практике таймеры почти всегда задерживаются. Причины могут быть разные: операционная система, другие фазы цикла событий или даже сама начальная фаза.
Не забывайте, что перед запуском цикла событий происходит начальная фаза, в которой функции регистрируются, но не вызываются. И если в этой фазе выполняются тяжелые операции (например, большой цикл for
), они могут значительно сдвинуть момент запуска таймеров.
Поэтому реальный результат будет выглядеть примерно так:
Таймер 0 мс задержан на 437 мс
Таймер 1 мс задержан на 442 мс
Таймер 500 мс задержан на 2 мс
Таймер 1000 мс задержан на 2 мс
Можно сказать, что наша тяжелая операция в начальной фазе заняла примерно 437 мс
, так как таймер, который должен был сработать сразу (через 0 мс
), сработал только через 437 мс
. Из-за этого задержался и второй таймер с задержкой в 1 мс
, ведь Node.js однопоточный, и все задачи выполняются последовательно.
Однако его задержка составила не 438 мс
, как можно было бы ожидать, а 442 мс
— даже на несколько мс больше. Это еще раз подтверждает, что таймеры в Node.js не всегда точны.
Теперь переходим к следующей фазе цикла событий — Pending Callbacks.
Pending Callbacks

Эта фаза отвечает за выполнение функций обратного вызова, которые были отложены на предыдущих итерациях цикла, чаще всего связанных с операциями I/O (например, обработка ошибок TCP).
На этом этапе происходит следующее:
- Если в фазе poll (которую мы рассмотрим позже) происходят системные события, например ошибки TCP, то их обработчики откладываются и выполняются на этой фазе.
- Node.js последовательно обрабатывает эти отложенные функции, пока очередь не опустеет или не будет достигнут лимит.
По сути, эта фаза выполняет роль "очистки" — здесь Node.js обрабатывает задачи, которые не удалось выполнить в предыдущих циклах.
Как уже упоминалось, на каждой итерации цикла событий эта фаза занимается отложенными задачами, перенесенными с предыдущих итераций. То есть, если обработчик был отложен, он будет выполнен на фазе Pending Callbacks следующей итерации.
Представьте, что вы пишете веб-сервер, который должен обрабатывать множество подключений:
const http = require("http");
const fs = require("fs");
// Создаем сервер
const server = http.createServer((req, res) => {
// Чтение файла (операция I/O)
fs.readFile("large-file.txt", (err, data) => {
if (err) {
// Если произошла ошибка, эта функция обратного вызова может быть
// отложена до фазы Pending Callbacks (будет выполнена на следующей итерации)
console.error("Error reading file:", err);
res.statusCode = 500;
res.end("Server error");
return;
}
res.statusCode = 200;
res.end(data);
});
});
// Обработка ошибок TCP-соединения
server.on("error", (err) => {
// Эта функция обратного вызова, скорее всего, будет обработана на фазе Pending Callbacks
console.error("Server error:", err);
});
server.listen(3000, () => {
console.log("Server running on port 3000");
});
В этом примере, если при запуске сервера произойдет ошибка TCP или при чтении файла возникнут проблемы, соответствующие обработчики могут выполниться не сразу, а на фазе Pending Callbacks.
Важно понимать суть этой фазы:
Если возникает ошибка, ее обработчик не выполняется мгновенно, а откладывается до следующей итерации цикла событий.
Но что если бы ошибки обрабатывались сразу? Это важный вопрос, связанный с архитектурными особенностями Node.js. Если бы мы пытались обрабатывать все ошибки мгновенно, в их исходной фазе (например, в фазе Poll), а не откладывали их на фазу Pending Callbacks, возникло бы несколько серьезных проблем:
- Самая серьезная проблема — возможная блокировка цикла событий. Некоторые системные ошибки и обработчики требуют значительного времени на выполнение. При их синхронной обработке цикл событий мог бы застрять на этих задачах и не перейти к другим операциям.
- Задержка I/O: во время фазы Poll Node.js сосредоточен на ожидании и обработке новых событий I/O. Если бы на этой фазе выполнялась сложная обработка ошибок, это могло бы замедлить обработку новых подключений и событий, что в итоге привело бы к таймаутам или разрывам соединений.
Но важно понимать, что когда мы откладываем выполнение функции обратного вызова на фазу Pending Callbacks, это не значит, что время на ее обработку исчезает — мы просто переносим ее на другой этап цикла событий. Зачем же тогда это нужно? Ведь такая задержка все равно может блокировать цикл событий при следующей итерации, не так ли?
Перенос части обработчиков на фазу Pending Callbacks позволяет Node.js контролировать момент выполнения потенциально тяжелых операций в цикле событий. Это обеспечивает более предсказуемую работу системы. Фаза Pending Callbacks находится сразу после таймеров и перед фазой Poll, поэтому:
- Сначала выполняются таймеры, для которых важно точное время срабатывания.
- Затем запускаются тяжелые системные обработчики, прежде чем цикл вернется к фазе опроса I/O.
Node.js и библиотека libuv — основа цикла событий Node.js, обеспечивающая кроссплатформенный доступ к асинхронным операциям I/O. Они устанавливают лимиты на количество отложенных функций обратного вызова, которые обрабатываются за одну итерацию. Если их слишком много, часть переносится на следующую итерацию. Это важно, потому что:
// Внутри libuv может происходить что-то вроде
while (pendingQueue.length > 0 && processedCallbacks < MAX_CALLBACKS_PER_ITERATION) {
const callback = pendingQueue.shift();
callback();
processedCallbacks++;
}
Это не дает фазе Pending Callbacks полностью занять одну итерацию цикла событий.
Таким образом, каждая фаза имеет свой механизм контроля, который не позволяет блокировать цикл событий. Если "определенные операции" выполняются на "правильной фазе", это значительно облегчает управление процессом и повышает общую производительность.
Существуют исключения из общего порядка обработки функций обратного вызова при работе с TCP-соединениями в Node.js. Иногда обработчики ошибок могут выполняться раньше, чем обработчики успешного выполнения, и это связано с особенностями работы цикла событий.
Если подключение неудачное, оно часто завершается очень быстро — например, если сервер не существует или порт закрыт, система определяет это практически мгновенно. В таком случае обработчик ошибок может выполниться сразу, минуя фазу Pending Callbacks.
С другой стороны, в некоторых ситуациях обработчики ошибок могут срабатывать позже (после обработчиков успешного выполнения). Это часто происходит при ошибках времени ожидания: если попытка подключения не получает ответа, необходимо дождаться истечения таймаута (который может длиться несколько секунд), прежде чем будет вызван обработчик ошибки. В таких случаях вы увидите, что обработчики успешного выполнения срабатывают раньше обработчиков ошибок.
Idle, prepare

Это фаза, которая идет сразу после Pending Callbacks — третья (четвертая с учетом начальной) фаза в цикле событий Node.js. Она служит внутренним нуждам самой платформы и не предназначена для прямого взаимодействия с пользователем.
Фаза состоит из двух этапов: Idle и Prepare.
Idle — это этап "простоя", когда Node.js выполняет фоновые задачи. Здесь особо нечего обсуждать, так как он просто запускается на каждой итерации цикла.
Этап Prepare — более важный. Он происходит сразу после Idle и перед фазой Poll. На этом этапе цикл событий готовится к предстоящим событиям и выполняет определенные запланированные обработчики.
Основная задача Prepare — выполнить те обработчики, которые должны сработать до начала опроса I/O на фазе Poll. Это ключевой момент, позволяющий Node.js подготовиться к потенциальному ожиданию I/O без блокировок.
Функции обратного вызова на этом этапе — внутренние, а не пользовательские. Например, здесь запускаются таймеры или настраиваются сетевые обработчики. Этот этап гарантирует, что все подготовлено, прежде чем цикл событий перейдет к ожиданию новых событий.
Пример запуска TCP-сервера:
const net = require('net');
const server = net.createServer((socket) => {
socket.end('Hello world\n');
});
server.listen(3000);
Перед тем как цикл событий перейдет к фазе Poll, Node.js может выполнить некоторые внутренние настройки на этапе Prepare. Это необходимо для того, чтобы сервер был готов принимать новые подключения. Речь идет не о пользовательских функциях обратного вызова, а о внутренних операциях, которые Node.js использует для управления базовым поведением системы.
Poll

Эта фаза, вероятно, самая важная в цикле событий Node.js, поскольку именно здесь обрабатывается основной поток I/O.
На фазе Poll происходит два ключевых процесса:
- Проверка событий I/O. Node.js активно отслеживает завершенные операции I/O. Можно представить это как официанта, который постоянно обходит столики в ресторане, спрашивая: "Вам что-нибудь нужно?" Так и Node.js проверяет:
- поступили ли новые сетевые запросы
- завершены ли операции чтения/записи файлов
- появились ли новые подключения
Например, когда пользователь заходит на ваш сайт или когда вы читаете файл, Node.js отправляет эти запросы операционной системе и ждет их завершения. Фаза Poll — это момент, когда Node.js проверяет результаты этих операций.
- Выполнение функций обратного вызова для завершенных операций. Когда операция завершена (например, файл был прочитан или пришел сетевой запрос), Node.js вызывает функцию-обработчик, связанную с этой операцией. Примеры таких обработчиков:
-
onRead
: "логирует" данные, когда файл был прочитан -
onConnected
: выполняет действия при установлении соединения -
onListen
: запускает определенный код, когда сокет начинает прослушивать подключения
Динамические импорты (использующие import()
) также обрабатываются на этой фазе.
Есть важный момент, который нужно понять: Node.js может блокироваться на фазе Poll. Это означает, что цикл событий может "застревать" здесь в ожидании I/O событий.
Когда приложению больше нечего обрабатывать, оно остается на фазе Poll, ожидая новых обработчиков I/O. Такая блокировка продолжается до тех пор, пока не произойдет одно из следующих событий:
- поступит новое событие I/O
- наступит время выполнения таймера (из фазы Timers)
- появятся готовые к выполнению обработчики от
setImmediate()
Такое поведение — намеренная особенность Node.js. Оно позволяет эффективно "ждать" новые задачи, не тратя лишние ресурсы на постоянное прохождение всех фаз цикла событий, когда делать нечего.
При этом важно понимать, что блокировка на фазе Poll не означает, что все приложение "замораживается". Это скорее "правильная блокировка" — эффективное ожидание следующей задачи.
Нужно быть осторожными с операциями, которые сильно нагружают процессор и выполняются в обратных вызовах этой фазы. Например:
// Это может заблокировать весь цикл событий, если файл слишком большой
fs.readFile('large-file.txt', (err, data) => {
// Ресурсоемкая операция в обработчике
const result = performComplexCalculation(data);
console.log(result);
});
Для ресурсоемких задач можно использовать:
- рабочие потоки (worker threads) — это выходит за рамки данной статьи, но вы можете изучить их самостоятельно
- дочерние процессы (child processes) — тоже на самостоятельное изучение
- разделение работы на небольшие части с помощью
setImmediate()
— о ней мы поговорим в следующем разделе
На фаза Poll Node.js большую часть времени обрабатывает операции I/O, такие как:
- обработка HTTP-запросов
- чтение и запись данных в базы
- выполнение операций с файлами
Check

В отличие от фаз Idle и Prepare, фазу Check мы, как пользователи, можем контролировать — именно здесь можно запланировать выполнение функций обратного вызова. Эта фаза запускается сразу после Poll, то есть сразу после операций I/O.
Для планирования обработчиков на фазу Check используется функция setImmediate()
. Ее применяют, когда нужно получить предсказуемый порядок выполнения — функция сработает сразу после фазы Poll.
Допустим, у нас есть такой код:
const readFileCallback = (err, data) => {
console.log(`readFileCallback ${data}`);
};
const f = "test.txt";
fs.readFile(f, readFileCallback);
setImmediate(() => console.log("setImmediate called!"));
Разберем, что происходит при выполнении этого кода.
Сначала идет начальная фаза, верно? На этой фазе мы регистрируем все необходимое, но файл еще не читается, и функция обратного вызова не вызывается — это происходит на фазе Poll. Т.е. мы планируем операции (чтение файла и вызов обработчика) к выполнению на фазе Poll.
Мы все еще находимся в начальной фазе и продолжаем разбирать код и распределять задачи по соответствующим фазам.
Встречается функция setImmediate()
. Что она делает? Она планирует выполнение своего обработчика на фазу Check.
Начальная фаза завершается, переходим к фазе Timers — там ничего не происходит, т.к. таймеров у нас нет. Переходим к фазе Pending Callbacks — тоже ничего нет, двигаемся дальше. Затем следуют фазы Idle и Prepare — Node.js выполняет внутренние задачи, к нам это не относится.
Наконец, наступает фаза Poll.
На фазе Poll происходят две основные вещи: сначала запускается чтение файла, затем должен выполниться соответствующий обработчик. Но прежде чем вызвать обработчик, нужно инициировать операцию чтения файла. После запуска операции чтения Node.js сразу же переходит к следующей фазе, потому что обработчик, запланированный через setImmediate()
, ожидает выполнения в фазе Check. Node.js не хочет тратить время и блокировать цикл событий, дожидаясь завершения чтения файла и выполнения обработчика.
Поэтому цикл событий переходит в фазу Check, где выполняется обработчик setImmediate()
и выводится сообщение setImmediate called!
. После этого цикл возвращается к фазам Timers и Poll. Когда мы снова попадем в фазу Poll, предположим, что чтение файла уже завершилось. Тогда на этой второй итерации цикла вызывается обработчик, связанный с завершением операции чтения файла.
Вот что происходит на самом деле:
Node.js переходит на фазу Poll и обращается к операционной системе с запросом: "Какие операции I/O завершились?". Для каждой завершенной операции он сразу же выполняет связанный с ней обработчик. Если в фазе Poll нет ожидающих обработчиков, НО при этом есть запланированные через setImmediate()
задачи, то Node.js выйдет из фазы Poll, чтобы обработать их.
Таким образом, в фазе Poll Node.js выполняет две основные задачи: проверяет завершенные операции I/O и запускает соответствующие обработчики. Но важно понимать, что он не ждет завершения каждой операции I/O по отдельности, а просто периодически проверяет, что уже завершилось с момента последней проверки.
Есть важный момент! Если файла не существует, то мы не сможем инициировать операцию чтения, и в этом случае функция обратного вызова будет выполнена немедленно. Это значит, что setImmediate()
не будет выполнен первым. И это логично: если файл отсутствует, то тяжелой операции чтения не будет, а значит, нам не нужно блокировать цикл событий, ожидая завершения чтения. В этом случае нет необходимости сначала переходить к фазе Check.
Попробуйте выполнить приведенный ниже код и посмотрите, каким будет результат.
Файл test.txt
существует:
const fs = require(`fs`);
const readFileCallback = (err, data) => {
console.log(`readFileCallback ${data}`);
};
const f = "test.txt";
fs.readFile(f, readFileCallback);
setImmediate(() => console.log("setImmediate called!"));
Вывод будет таким:
setImmediate called!
readFileCallback data
Файл test123.txt
не существует:
const fs = require(`fs`);
const readFileCallback = (err, data) => {
console.log(`readFileCallback ${data}`);
};
const f = "test123.txt";
fs.readFile(f, readFileCallback);
setImmediate(() => console.log("setImmediate called!"));
Вывод будет таким:
readFileCallback undefined
setImmediate called!
Close Callbacks

После того как Node.js обработал все таймеры, функции обратного вызова I/O, внутренние операции и функции setImmediate()
, остается еще один финальный этап — фаза Close Callbacks.
Эта фаза необходима для окончательной "уборки" перед завершением обработки событий. При закрытии сервера или разрыве соединения с сокетом Node.js необходимо корректно завершить все связанные с этим процессы.
Фаза Close Callbacks отвечает за выполнение следующих задач:
- завершение работы TCP сервера с помощью
server.close()
- закрытие соединения с сокетом через
socket.on('close', ...)
- завершение выполнения процесса
Например, если вы создали объект net.Socket
и добавили к нему обработчик события .on('close', ...)
, этот обработчик не выполнится сразу при закрытии соединения, а дождется этой особой фазы.
Рассмотрим пример:
const net = require("net");
const server = net.createServer((socket) => {
console.log("Клиент подключился");
socket.on("close", () => {
console.log("Сокет закрыт — этот код выполняется на фазе Close Callbacks");
});
// Закрываем сокет через 2 секунды
setTimeout(() => {
socket.end(); // Запускает событие 'close'
}, 2000);
});
server.listen(3000, () => {
console.log("Сервер запущен и слушает порт 3000");
});
Вот что происходит:
- клиент подключается к серверу
- через 2 секунды сервер закрывает соединение с этим клиентом
- при закрытии соединения генерируется событие
close
- Node.js ждет, пока цикл событий дойдет до фазы Close Callbacks, чтобы выполнить обработчик для
.on('close', ...)
Только на этой фазе выполняется функция, привязанная к .on('close', ...)
.
Таким образом, последний console.log()
не сработает сразу после вызова .end()
, а выполнится только тогда, когда Node.js дойдет до фазы Close Callbacks.
Мы рассмотрели все фазы, но осталось еще кое что, о чем мы не говорили. Это своего рода "скрытая" фаза — речь идет о process.nextTick()
.

Как видно на изображении, process.nextTick()
выполняется после каждой фазы. Звучит неожиданно, правда? На самом деле цикл событий выглядит примерно так: timers -> process.nextTick()
-> pending callbacks -> process.nextTick()
и т.д.
На самом деле, process.nextTick()
запускается даже раньше, чем Timers. Потому что перед циклом событий есть начальная фаза. И process.nextTick()
также срабатывает сразу после нее.
Возможно, вы заметили, что process.nextTick()
не был показан на первой схеме цикла событий, хотя это часть асинхронного API. Это потому, что технически process.nextTick()
не является частью цикла событий. nextTickQueue
обрабатывается сразу после завершения текущей операции, вне зависимости от текущей фазы цикла.
process.nextTick()
— очень мощный инструмент. Если вспомнить, перед циклом событий есть "начальная фаза" — основной модуль (main module). После ее завершения начинается сам цикл событий. Поэтому nextTick()
можно использовать для определения момента завершения начальной фазы. Можно было бы запустить таймеры, чтобы увидеть, когда стартует цикл событий, ведь Timers — это первая фаза цикла. Но таймеры могут работать с задержкой, а nextTick()
дает более точные результаты.
Рассмотрим несколько примеров.
console.log("start");
for (let i = 0; i < 1000000; i++);
console.log("end");
setTimeout(() => console.log("timer"), 0);
process.nextTick(() => console.log("nextTick"));
Какой будет результат?
start
end
nextTick
timer
Почему так? Потому что сразу после начальной фазы запускается обработчик nextTick()
, и только после этого начинается цикл событий, то есть фаза Timers.
let val;
function test() {
console.log(val);
}
test();
val = 1;
Весь этот код синхронный — ничего сложного. Сначала мы создаем переменную val
, но она еще не инициализирована. Затем, до того как присвоить val
значение 1
, мы вызываем функцию, которая выводит val
— а на тот момент val
все еще не определена, поэтому в консоль выводится undefined
. Но что, если исправить это очень интересным способом?
let val;
function test() {
console.log(val);
}
process.nextTick(test);
val = 1;
А теперь, как думаете, что произойдет? process.nextTick()
не выполнится в рамках начальной фазы, но сработает сразу после нее. Значит, он выполнится уже после того, как переменной val
будет присвоено значение 1
. Поэтому теперь в консоли появится 1
.
Есть две главные причины использовать process.nextTick()
:
- Необходимо обработать ошибки, освободить ненужные ресурсы или, возможно, повторить запрос перед продолжением цикла событий.
- Иногда требуется запустить обработчик после того, как стек вызовов очистится, но до того, как цикл событий перейдет к следующей фазе.
И еще одна важная деталь — обещаю, что последняя: в Node.js промисы работают в очереди микрозадач (microtask queue), но с меньшим приоритетом, чем обработчики process.nextTick()
. Когда промис разрешается, его обработчики .then()
ставятся в очередь микрозадач, которая выполняется после всех nextTick-обработчиков, но до перехода цикла событий к следующим фазам (таймерам или операциям I/O). Такая приоритизация обеспечивает предсказуемый порядок выполнения цепочек промисов и сохраняет неблокирующую природу Node.js.
Если вы знакомы с тем, как JS работает в браузере, то для Node.js можно провести следующие аналогии:
- очередь макрозадач браузера → распределяется по разным фазам в Node.js
- очередь микрозадач браузера → используется для обработчиков промисов и
process.nextTick()
в Node.js
Важное отличие Node.js: операции process.nextTick()
обрабатываются раньше других промисов из очереди микрозадач и проверяются в конце каждой фазы.
Думаю, теперь у вас есть полное понимание того, как работает Node.js.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩