Что такое Node.js?

Согласно официальной документации Node, Node.js — это среда выполнения JavaScript, построенная на движке JavaScript V8 с открытым исходным кодом от Google. Node.js может выполнять код JavaScript на стороне сервера, что позволяет создавать быстрые, масштабируемые и высокопроизводительные сетевые приложения.

История Node.js

Node.js был представлен в 2009 году Райаном Далем. Несмотря на первоначальный скептицизм, эта концепция приобрела популярность в 2011 году, когда Даль представил Node.js на встрече PHP в Сан-Франциско 22 февраля 2011 года. Видео стало вирусным, привлекая внимание к использованию JavaScript на стороне сервера. В этом видео Райан Даль объяснил Node.js перед группой программистов. Он объяснил, как он разработал новую среду выполнения JavaScript с помощью движка v8 Google Chrome и как мы можем использовать JavaScript на стороне сервера. До этого JavaScript использовался только на стороне клиента и браузера. Выслушав объяснение Райана, большинство программистов высмеяли его, потому что они не могли принять JavaScript в качестве серверного языка. В то время существовало несколько горячих серверных языков, таких как Ruby on Rails. Но Райан Даль не остановился. Даль увидел потенциал использования JavaScript на стороне сервера и разработки и улучшения Node.js в качестве решения.

Причины популярности Node.js

Произошел критический инцидент, который помог узлу завоевать популярность.

  • В 2010 году Исаак З. Шлютер создал NPM (сокращение от Node Package Manager). В то время Node.js была относительно новой платформой и не имела стандартного менеджера пакетов, подобного тем, которые используются в других языках программирования. Разработчикам стало проще делиться своим кодом с другими разработчиками и повторно использовать существующие пакеты в своих проектах.
  • В 2007 Дуайт Мерриман и Элиот Горовиц создали MongoDB. Он использует двоичную структуру данных в стиле JSON для хранения данных, которую легко использовать с JavaScript. Как и ожидалось, сообщества разработчиков не приняли базу данных NoSQL, такую ​​как MongoDB, потому что в то время была популярна реляционная база данных SQL. По мере того как социальные сети, такие как Twitter и Facebook, начали публиковать API в формате JSON, количество структур данных в стиле JSON увеличилось, поскольку разработчикам стало сложнее работать с реляционными базами данных и данными в формате JSON. В результате такие решения, как MongoDB и Node.js, получили более широкое распространение.
  • Node.js упростил разработчикам использование JavaScript как во внешнем, так и во внутреннем интерфейсе своих веб-приложений. Эта возможность стала важным фактором популярности Node.js среди разработчиков. Используя один и тот же язык как на стороне клиента, так и на стороне сервера, Node.js устраняет необходимость переключения между несколькими языками программирования, что приводит к более упорядоченной и эффективной разработке. Кроме того, поскольку JavaScript является широко используемым языком, многие разработчики уже имеют опыт работы с ним, что делает переход к использованию Node.js для серверной разработки естественным выбором. Эти факторы способствовали тому, что Node.js стал популярным решением для создания быстрых, масштабируемых и высокопроизводительных сетевых приложений.

Архитектура Node.js и понимание движка V8

Давайте узнаем об архитектуре узла относительно зависимостей узла.

Node.js имеет несколько зависимостей для правильной работы. Наиболее важными из них являются двигатель v8 и libuv. Ранее мы видели, что Node.js был построен на движке Chrome v8. Во-первых, мы должны знать роль двигателя v8 здесь.

Многие из нас пишут JavaScript для браузера. Когда мы пишем код, машины не понимают наш код напрямую. Машина должна преобразовать его в машинный код, чтобы понять, что мы командуем. В браузере есть движки, которые понимают наш код как машинный код. Разные браузеры имеют разные механизмы для понимания/преобразования кода JavaScript в машинный код. Вот некоторые примеры:

  • Гугл Хром: V8
  • Mozilla Firefox: обезьяна-паук
  • Apple Safari: JavaScriptCore
  • Microsoft Edge: чакра
  • Opera: Blink (основан на том же движке, что и Google Chrome, V8)

Самый мощный из них — движок Chrome v8.

На стороне сервера должно быть что-то, что преобразует код JavaScript в машинный код. Для этого Райан Даль использовал сверхскоростной движок V8 в Node.js. Благодаря этому Node.js может преобразовывать код JavaScript в машинный код. Это вариант использования движка V8 в Node.

Библиотека Либув

Движка V8 недостаточно для запуска такой серверной среды, как Node.js. Вот еще одна важная библиотека под названием «libuv». Это библиотека с открытым исходным кодом, которая сильно ориентирована на асинхронный ввод-вывод (ввод/вывод).

С помощью libuv Node.js может получить доступ к базовой операционной системе компьютера, файловой системе, материалам, связанным с сетью, и многому другому.

«libuv» реализует две важные функции Node.js —

  1. Цикл событий, который обрабатывает простые задачи, такие как обратный вызов или сетевой ввод-вывод.
  2. Пул потоков для выполнения тяжелой работы, такой как доступ к файлам или их сжатие.

Я подробно расскажу об этих двух функциях позже.

Node.js полностью написан на JavaScript?

Мы интуитивно думаем, что за кулисами Node.js находится только код JavaScript, поскольку Node.js — это среда выполнения JavaScript. Но это не так. Как я уже сказал, libuv — неотъемлемая часть Node, а libuv полностью написана на C++. С другой стороны, V8 также написан на JS и C++. Итак, Node написан не только на JavaScript, но и на C++. Но есть некоторые уровни абстракции, поэтому мы можем получить доступ ко всем функциям и всему в узле с помощью чистых функций JavaScript. Нам не нужно возиться с C++ или любым лежащим в его основе кодом. Например, для чтения файлов мы пишем чистую js-функцию для чтения из файловой системы, функциональность которой написана в libuv на C++. Довольно интересно.

Важно отметить, что Node.js зависит не только от движка V8 и libuv. Существуют и другие библиотеки, например — парсер HTTP (для разбора HTTP), c-ares (для обработки DNS-запросов), OpenSSL (для криптографии), zlib (для сжатия файлов) и т. д.

Теперь давайте посмотрим, что такое поток и пул потоков.

Поток и пул потоков

Процесс узла инициируется на нашем компьютере всякий раз, когда мы используем Node.js. Этот процесс представляет программу, которая в данный момент выполняется.

В рамках этого процесса узел работает в одном потоке. Поток похож на мини-программу внутри большой программы, последовательность инструкций, которая может выполнять несколько задач одновременно. Это как ящик, в котором выполняется наш код.

Очень важно четко понимать концепции многопоточности и однопоточности. Многопоточность позволяет выполнять несколько операций одновременно, при этом несколько задач могут выполняться в фоновом режиме одновременно. С другой стороны, однопоточность позволяет выполнять только одну операцию за раз. Другими словами, он работает в линейной последовательности и не может выполнять несколько операций одновременно.

Важно понимать, что Node работает в одном потоке, независимо от того, сколько у нас пользователей. Это означает, что если отдельный поток будет заблокирован, это может повлиять на все приложение. Итак, нам нужно быть особенно внимательными к действиям, которые потенциально могут заблокировать поток.

Давайте посмотрим, что произойдет, если узел запустится в одном потоке.

Когда приложение Node запускается, оно проходит несколько этапов. Node.js инициализирует программу, выполняет весь код верхнего уровня, требует необходимые модули, а затем регистрируются обратные вызовы событий.

После выполнения всех этих шагов запускается цикл событий. Цикл событий, в котором выполняется большая часть работы приложения. Это сердце приложения. Но некоторые задачи могут быть ресурсоемкими и тяжелыми. Они потенциально могут заблокировать отдельный поток, если выполняются в цикле обработки событий. Обработка этих тяжелых задач вне цикла событий важна, чтобы избежать этого.

Здесь на помощь приходит пул потоков, который предоставляет библиотека libuv. Цикл событий выгружает эти тяжелые задачи в пул потоков. Это происходит за кулисами. Нам не нужно беспокоиться о том, какие задачи попадут в пул потоков, а какие нет.

В пуле потоков есть четыре дополнительных потока, отдельных от одного основного потока. Мы можем настроить пул потоков до 128 потоков. Но, как правило, нам это не нужно. Достаточно четырех потоков.

Вот несколько примеров задач, которые выгружаются в пул потоков из цикла событий:

  1. Задачи, использующие API файловой системы. Когда мы выполняем такие операции, как чтение или запись файлов, эти задачи могут занимать значительное время. Они могут блокировать один поток и вызывать проблемы с производительностью.
  2. Задача, связанная с криптографией. Криптография включает в себя сложные математические вычисления. И выполнение этих операций в Event Loop может замедлить работу нашего приложения.
  3. Задачи сжатия. Когда нам нужно сжать большие данные, такие как изображения или видео, эти задачи могут занять много времени, а также могут замедлить цикл обработки событий. Поэтому эти задачи выгружаются в пул потоков, чтобы предотвратить это.
  4. Поиск DNS: когда нам нужно преобразовать доменное имя в IP-адрес, это делается с помощью процесса поиска DNS. Этот процесс также может занять время и заблокировать цикл событий, что может привести к проблемам с производительностью. Поэтому, чтобы предотвратить это, эти задачи выгружаются в пул потоков, где они могут выполняться одновременно, не блокируя основной поток.

Цикл событий и его реализация в Node.js

Теперь мы находимся в процессе Node.js. Этот процесс содержит поток, в котором выполняется цикл обработки событий. Цикл событий является важным компонентом процесса Node.js. Он отвечает за выполнение всего кода в функциях обратного вызова, составляющих приложение Node.js.

Как мы узнали ранее, некоторые задачи также могут быть выгружены в пул потоков для эффективной обработки. Но цикл событий — это центральный механизм среды выполнения Node.js. Node.js основан на архитектуре, основанной на обратном вызове. Это означает, что функции вызываются, как только определенное событие завершилось или было сгенерировано. Это происходит из-за управляемой событиями архитектуры Node.js.

Архитектура, управляемая событиями

Архитектура, управляемая событиями, — это система, в которой события генерируются, перехватываются и обрабатываются циклом событий. Затем цикл событий выполняет связанные функции обратного вызова для этого события.

Например, такие события, как новый HTTP-запрос, истечение таймера или завершение чтения файла или записи в файловую операцию, будут генерировать события, которые затем будут обрабатываться циклом событий. Затем он обрабатывает эти события, выполняя связанные функции обратного вызова. Цикл событий наблюдает за событиями и выполняет необходимую оркестровку за кулисами.

Давайте подробнее рассмотрим, как на самом деле работает цикл обработки событий:

Цикл событий запускается, как только запускается приложение Node.js. Цикл событий состоит из нескольких фаз. Каждая фаза имеет свою собственную очередь обратного вызова. Кто-то может сказать, что цикл событий имеет только один обратный вызов или очередь событий. Но у него есть несколько фаз, каждая со своей собственной очередью обратного вызова.

Четыре наиболее важные фазы цикла событий:

  1. Обработка обратных вызовов таймеров с истекшим сроком действия (setTimeout()). На первом этапе выполняются обратные вызовы таймеров с истекшим сроком действия. Если таймер истекает во время любой из других фаз, связанная с ней функция обратного вызова будет выполнена, как только завершится текущая фаза, и цикл обработки событий вернется к своей первой фазе.
  2. Опрос ввода-вывода и выполнение обратных вызовов ввода-вывода. Узловой ввод-вывод означает сетевые или файловые операции, такие как чтение файлов, запись в новые файлы и т. д., а опрос означает поиск новых событий ввода-вывода, которые готовы к обработке и помещают их в очередь обратного вызова. На этом этапе выполняется 99% кода приложения.
  3. Выполнение обратных вызовов SetImmediate. Это особый тип таймера, который позволяет выполнять код сразу после выполнения обратных вызовов ввода-вывода.
  4. Выполнение обратных вызовов закрытия. Цикл обработки событий обрабатывает все события закрытия на этом этапе. Сюда входят такие события, как закрытие веб-сервера или веб-сокета.

В дополнение к этим четырем фазам важно знать о двух других очередях:

Очередь process.nextTick(): эта очередь выполняет свои обратные вызовы сразу после завершения текущей фазы. Это похоже на обратные вызовы SetImmediate. Единственное отличие состоит в том, что обратные вызовы SetImmediate выполняются только сразу после выполнения обратных вызовов ввода-вывода, а process.nextTick() выполняется сразу после любой из фаз.

Очередь микрозадач для разрешения обещаний: эта очередь выполняет свои обратные вызовы сразу после завершения текущей фазы, как и очередь process.nextTick(). Если в любой из этих двух очередей есть какие-либо обратные вызовы, они будут выполняться сразу после завершения текущей фазы, а не ждать, пока весь цикл обработки событий завершит свои четыре фазы. Например, если обещание разрешает и возвращает данные из вызова API во время выполнения обратного вызова таймера с истекшим сроком действия, его обратный вызов будет выполнен сразу после завершения обратного вызова таймера.

После каждой из первых четырех фаз цикл обработки событий проверяет наличие обратных вызовов в этих двух очередях. Если они есть, они будут немедленно казнены. Это завершает один тик цикла событий. Тик определяется как один цикл цикла. После одного цикла среда выполнения Node.js проверяет наличие ожидающих таймеров или задач ввода-вывода или каких-либо обратных вызовов каких-либо фаз. Если они есть, цикл выполняется снова. В противном случае приложение закрывается.

Асинхронное программирование стало возможным в Node.js благодаря циклу событий, который делает цикл событий наиболее важной функцией Node.js. Это позволяет Node.js работать в одном потоке, что делает его легким и быстрым, но также представляет некоторые потенциальные риски. Поскольку Node.js — однопоточная программа, можно случайно заблокировать наше приложение. Поэтому важно проявлять особую осторожность при написании кода, чтобы избежать случайной блокировки приложения.

Действия, чтобы избежать блокировки цикла событий

Вот несколько советов, как избежать блокировки кода в Node.js:

  • Избегайте использования синхронных версий функций; написать любой необходимый синхронный код вне каких-либо функций обратного вызова. Таким образом, код будет выполнен до запуска цикла обработки событий.
  • Избегайте сложных вычислений, таких как вложенные циклы в Node.js, так как они могут привести к блокировке приложения.
  • Будьте осторожны при работе с большими объектами JSON, так как их синтаксический анализ или преобразование в строки может занять много времени.
  • Избегайте использования сложных регулярных выражений, так как они требуют большой вычислительной мощности, которая может быть ресурсоемкой и замедлять цикл обработки событий. Если мы используем сложное регулярное выражение, его выполнение может занять значительное время. Вот почему также рекомендуется разбивать большие регулярные выражения на более мелкие и простые, которые могут выполняться более эффективно. Это гарантирует, что цикл обработки событий будет продолжать работать без сбоев, а приложение останется отзывчивым.
  • Не выполняйте какие-либо задачи с интенсивным использованием ЦП в цикле событий. Это может привести к блокировке приложения. Вот несколько примеров задач, интенсивно использующих ЦП:
  1. Тяжелые вычисления, такие как математические расчеты и научное моделирование
  2. Обработка изображений и видео, например изменение размера, обрезка и фильтрация изображений и видео.
  3. Криптографические операции, такие как шифрование и дешифрование данных
  4. Сжатие и распаковка данных
  5. Важно быть осторожным, если мы хотим выполнять эти задачи в Node.js. Нам придется предпринять дополнительные шаги, чтобы свести к минимуму их влияние на цикл событий. Это можно сделать, вручную перенеся эти задачи в отдельный процесс или поток или используя внешнюю библиотеку, оптимизированную для выполнения этих типов операций.

Кроме того, важно структурировать код приложения таким образом, чтобы не оказывать давления на цикл обработки событий, например, избегая сложных алгоритмов, сводя к минимуму блокирующие операции и по возможности используя методы асинхронного программирования. Это поможет гарантировать, что наше приложение Node.js останется быстрым, эффективным и отзывчивым даже при большой нагрузке.

Вот и все на сегодня!

Поздравляем с окончанием этой статьи! Теперь у вас есть более глубокое понимание Node.js. Будь то новичок или опытный разработчик, эта информация поможет вам создавать эффективные, масштабируемые и высокопроизводительные приложения.

Если вам понравилось это читать, вы можете связаться со мной в Твиттере или ознакомиться с моими другими статьями. Кроме того, вы можете подписаться на мою нетехническую рассылку, где я делюсь своими мыслями о жизни, продуктивности и многом другом, а также лучшим контентом, который мне попадался каждый месяц.

Счастливого обучения!

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.