В этом году, на конференции Forward.js, посвящённой JavaScript, я выступал с докладом «You don’t know Node». Во время выступления я задал аудитории несколько вопросов о Node, и большинство присутствующих не смогли ответить на многие из них. А ведь мой доклад слушали технические специалисты. Никаких подсчётов я не производил, но выглядело всё именно так, да и несколько слушателей, которые подошли ко мне после выступления, это подтвердили.
Проблема, которая заставила меня сделать то выступление, заключается в том, что, по-моему, система обучения Node выстроена неправильно. Большинство учебных материалов сосредоточено на пакетах Node, но не на самой платформе. Часто этих пакеты служат обёртками для модулей Node (вроде http
или stream
). Как результат, тот, кто не знает Node и сталкивается с проблемой, источником которой может оказаться не некий пакет, а платформа, оказывается в крайне невыгодном положении.
Я выбрал несколько вопросов и ответов с той конференции и включил их в эту статью. Сами вопросы представлены в заголовках разделов статьи. Попытайтесь, прочтя вопрос, не читать дальше, а сначала мысленно на него ответить. Если вы найдёте ошибку в моих ответах — пожалуйста дайте мне знать.
Вопрос №1. Что такое стек вызовов и является ли он частью движка V8?
Стек вызовов (Call Stack) определённо является частью V8. Это — структура данных, которую V8 использует для отслеживания вызовов функций. Каждый раз, когда мы вызываем функцию, V8 помещает ссылку на эту функцию в стек вызовов, а когда из этой функции вызываются другие функции, продолжает делать то же самое со ссылками на них. Кроме того, в стек попадают и функции, которые вызывают сами себя рекурсивно.
Стек вызовов. Скриншот из моего курса на Pluralsight, посвящённого продвинутому изучению Node.js
Когда, при вложенных вызовах функций, функция завершает выполнение, V8 извлекает ссылку на функцию из верхней части стека вызовов и подставляет возвращённое ей значение туда, куда требует логика программы.
Почему это важно понимать при работе с Node? Дело в том, что на один процесс Node приходится только один стек вызовов. Если стек будет полон, процесс окажется нагружен какой-то работой. Об этом стоит помнить.
Вопрос №2. Что такое цикл событий? Является ли он частью движка V8?
Как вы думаете, где на следующем рисунке изображён цикл событий (event loop)?
Окружение V8. Скриншот из моего курса на Pluralsight, посвящённого продвинутому изучению Node.js
Цикл событий реализован в библиотеке libuv
. Он не является частью V8.
Цикл событий — это сущность, которая обрабатывает внешние события и преобразует их в вызовы коллбэков. Это — довольно сложно устроенный цикл, который берёт события из очереди событий и помещает их коллбэки в стек вызовов.
Если вы впервые слышите о цикле событий, вышеприведённые рассуждения могут оказаться не особенно вразумительными. Цикл событий является частью гораздо более общей картины:
Цикл событий. Скриншот из моего курса на Pluralsight, посвящённого продвинутому изучению Node.js
Для того, чтобы понять сущность цикла событий, полезно знать о том, в какой среде он работает. Нужно понимать роль V8, знать об API Node, и о том, как работает очередь событий, код, связанный с которыми, выполняется в V8.
API Node — это функции, вроде setTimeout
или fs.readFile
. Они не являются частью JavaScript. Это — просто функции, доступ к которым даёт нам Node.
Цикл событий находится в центре всего этого (конечно, на самом деле, всё это устроено сложнее) и играет роль организатора. Когда стек вызовов V8 пуст, цикл событий может принять решение о том, что следует выполнять дальше.
Вопрос №3. Что будет делать Node, когда стек вызовов и очереди цикла событий окажутся пустыми?
Ответ прост: Node просто завершит работу.
Когда вы запускаете приложение, Node автоматически запускает цикл событий, а когда цикл событий простаивает, когда ему нечего делать, процесс завершает работу.
Для того, чтобы процесс Node не завершался, нужно поместить что-нибудь в очередь событий. Например, когда вы запускаете таймер или HTTP-сервер, вы сообщаете циклу событий о том, что ему нужно продолжать работу и следить за этими событиями.
Вопрос №4. Помимо движка V8 и библиотеки libuv, какие ещё внешние зависимости есть у Node?
Вот некоторые самостоятельные библиотеки, которые может использовать процесс Node:
http-parser
c-ares
OpenSSL
zlib
Все они, по отношению к Node, являются внешними. У них имеется собственный исходный код, их распространение регулируется отдельными лицензиями. Node просто их использует.
Об этом стоит помнить для того, чтобы знать, где именно выполняется код вашей программы. Если вы, например, занимаетесь сжатием данных, вы можете столкнуться с проблемой, которая произошла в недрах стека zlib
. Возможно, причина — в ошибке библиотеки, поэтому не стоит валить всю вину на Node.
Вопрос №5. Можно ли запустить процесс Node без V8?
Это хитрый вопрос. Для запуска процесса Node нужен JS-движок, но V8 — это не единственный доступный движок. В качестве альтернативы можно воспользоваться Chakra.
Взгляните на этот Github-репозиторий для того, чтобы узнать подробности о проекте node-chakra
.
Вопрос №6. В чём разница между module.exports и exports?
Для экспорта API модулей всегда можно пользоваться командой module.exports
. Можно, за исключением одной ситуации, использовать и exports
:
module.exports.g = ... // Ok
exports.g = ... // Ok
module.exports = ... // Ok
exports = ... // Совсем не Ok
Почему?
Команда exports —
это просто ссылка, псевдоним для конструкции module.exports
. Когда вы пытаетесь записать что-нибудь непосредственно в exports
, вы меняете ссылку, которая там хранится, как результат, при последующих обращениях к exports
вы уже не работаете с тем, на что эта переменная ссылается в официальном API (а это — module.exports
). Записав что-нибудь в exports
, вы превращаете это ключевое слово в локальную переменную, находящуюся в области видимости модуля.
Вопрос №7. Почему в модулях переменные верхнего уровня не являются глобальными?
Предположим, у вас имеется модуль module1
, в котором определена переменная верхнего уровня g
:
// module1.js
var g = 42;
Далее, есть ещё один модуль, module2
, к которому подключают module1
и пытаются обратиться к переменной g
, получая в ответ сообщение об ошибке g is not defined
.
Почему? Ведь, если сделать то же самое в браузере, то, после подключения скриптов, к их глобальным переменным обращаться можно.
Каждый файл Node оборачивается в собственное немедленно вызываемое функциональное выражение (IIFE, Immediately Invoked Function Expression). Все переменные, объявленные в файле Node, оказываются внутри этого IIFE и снаружи не видны.
Вот вопрос, связанный с рассматриваемым вопросом: что будет выведено после запуска следующего файла Node, в котором имеется лишь одна строчка кода:
// script.js
console.log(arguments);
Очевидно, в консоль попадут какие-то аргументы!
Вывод аргументов
Почему? Дело в том, что этот файл Node выполняет как функцию. Node оборачивает код в функцию и у этой функции имеется пять аргументов, которые и можно видеть на рисунке.
Вопрос №8. Объекты exports, require и module глобально доступны в каждом файле, но каждый файл имеет их собственные экземпляры. Как такое возможно?
Когда вам нужен объект require
, вы просто вызывает его напрямую, так, как если бы он был глобальной переменной. Однако, если исследовать require
в двух разных файлах, окажется, что перед нами — два разных объекта. Почему это так?
Всё дело — в уже знакомых нам IIFE:
Исследование особенностей работы Node
Как видите, IIFE передаёт коду следующие пять аргументов: exports
, require
, module
, __filename
, и __dirname
.
Эти пять переменных кажутся глобальными при использовании их в Node, но они, на самом деле, являются обычными аргументами функции.
Вопрос №9. Что такое циклические зависимости модулей в Node?
Если у вас имеется модуль module1
, который зависит от module2
, а module2
, в свою очередь, зависит от module1
, что произойдёт? Будет выведено сообщение об ошибке?
// module1
require('./module2');
// module2
require('./module1');
Никакого сообщения об ошибке не будет. Node позволяет подобное.
Итак, в module1
подключается module2
, но так как в module2
подключается module1
, а module1
пока не полностью готов, module1
просто получит неполную версию module2
. Теперь вы об этом знаете.
Вопрос №10. Когда допустимо использовать синхронные методы для работы с файловой системой (вроде readFileSync)?
Каждый асинхронный метод объекта fs
в Node имеет синхронную версию. Зачем пользоваться синхронными методами вместо асинхронных?
Иногда в синхронных методах нет ничего плохого. Например, они могут пригодиться на этапе инициализации, при загрузке сервера. Часто ими так и пользуются, когда всё, что делается после инициализации, зависит от загруженных на этапе инициализации данных. Вместо того, чтобы заниматься конструированием кода, основанного на коллбэках, в подобных ситуациях, когда выполняется единоразовая загрузка каких-либо данных, вполне приемлемы синхронные методы.
Однако, если вы пользуетесь синхронными методами внутри обработчиков неких событий, вроде коллбэка HTTP-сервера, отвечающего за обработку запросов, то это, без вариантов, совершенно неправильно. Делать так настоятельно не рекомендуется.
Итоги
Надеюсь, вы смогли ответить на все эти вопросы, или, по крайней мере, на некоторые из них.
Уважаемые читатели! Если бы вы оказались на конференции по JS, на месте автора этой статьи, какие вопросы по Node.js вы задали бы аудитории?
Автор: ru_vds