Как работает JS: о внутреннем устройстве V8 и оптимизации кода
Перед вами — второй материал из серии, посвящённой особенностям работы JavaScript на примере движка V8. В первом шла речь о механизмах времени выполнения V8 и о стеке вызовов. Сегодня мы углубимся в особенности V8, благодаря которым исходный код на JS превращается в исполняемую программу, и поделимся советами по оптимизации кода.
О JS-движках
JavaScript-движок — это программа, или, другими словами, интерпретатор, выполняющий код, написанный на JavaScript. Движок может быть реализован с использованием различных подходов: в виде обычного интерпретатора, в виде динамического компилятора (или JIT-компилятора), который, перед выполнением программы, преобразует исходный код на JS в байт-код некоего формата.
Вот список популярных реализаций JavaScript-движков.
- V8 — движок с открытым исходным кодом, написан на C++, его разработкой занимается Google.
- Rhino — этот движок с открытым кодом поддерживает Mozilla Foundation, он полностью написан на Java.
- SpiderMonkey — это самый первый из появившихся JS-движков, который в прошлом применялся в браузере Netscape Navigator, а сегодня — в Firefox.
- JavaScriptCore — ещё один движок с открытым кодом, известный как Nitro и разрабатываемый Apple для браузера Safari.
- KJS — JS-движок KDE, который разработал Гарри Портен для браузера Konqueror, входящего в проект KDE.
- Chakra (JScript9) — движок для Internet Explorer.
- Chakra (JavaScript) — движок для Microsoft Edge.
- Nashorn — движок с открытым кодом, являющийся частью OpenJDK, которым занимается Oracle.
- JerryScript — легковесный движок для интернета вещей.
Почему был создан движок V8?

Движок с открытым кодом V8 был создан компанией Google, он написан на C++. Движок используется в браузере Google Chrome. Кроме того, что отличает V8 от других движков, он применяется в популярной серверной среде Node.js.
Логотип V8
При проектировании V8 разработчики задались целью улучшить производительность JavaScript в браузерах. Для того, чтобы добиться высокой скорости выполнения программ, V8 транслирует JS-код в более эффективный машинный код, не используя интерпретатор. Движок компилирует JavaScript-код в машинные инструкции в ходе исполнения программы, реализуя механизм динамической компиляции, как и многие современные JavaScript-движки, например, SpiderMonkey и Rhino (Mozilla). Основное различие заключается в том, что V8 не использует при исполнении JS-программ байт-код или любой промежуточный код.
О двух компиляторах, которые использовались в V8
Внутреннее устройство V8 изменилось с выходом версии 5.9, которая появилась совсем недавно. До этого же он использовал два компилятора:
- full-codegen — простой и очень быстрый компилятор, который выдаёт сравнительно медленный машинный код.
- Crankshaft — более сложный оптимизирующий JIT-компилятор, который генерирует хорошо оптимизированный код.
- Главный поток, который занимается тем, что от него можно ожидать: читает исходный JS-код, компилирует его и выполняет.
- Поток компиляции, который занимается оптимизацией кода в то время, когда выполняется главный поток.
- Поток профилировщика, который сообщает системе о том, в каких методах программа тратит больше всего времени, как результат, Crankshaft может эти методы оптимизировать.
- Несколько потоков, которые поддерживают механизм сборки мусора.
После того, как код какое-то время поработает, поток профилировщика соберёт достаточно данных для того, чтобы система могла понять, какие методы нужно оптимизировать.
Далее, в другом потоке, начинается оптимизация с помощью Crankshaft. Он преобразует абстрактное синтаксическое дерево JavaScript в высокоуровневое представление, использующее модель единственного статического присваивания (static single-assignment, SSA). Это представление называется Hydrogen. Затем Crankshaft пытается оптимизировать граф потока управления Hydrogen. Большинство оптимизаций выполняется на этом уровне.
Встраивание кода
Первая оптимизация программы заключается в заблаговременном встраивании в места вызовов как можно большего объёма кода. Встраивание кода — это процесс замены команды вызова функции (строки, где вызывается функция) на её тело. Этот простой шаг позволяет сделать следующие оптимизации более результативными.

Вызов функции заменяется на её тело
Скрытые классы
JavaScript — это язык, основанный на прототипах: здесь нет классов. Объекты здесь создаются с использованием процесса клонирования. Кроме того, JS — это динамический язык программирования, это значит, что, после создания экземпляра объекта, к нему можно добавлять новые свойства и удалять из него существующие.
Большинство JS-интерпретаторов используют структуры, напоминающие словари (основанные на использовании хэш-функций), для хранения сведений о месте расположения значений свойств объектов в памяти. Использование подобных структур делает извлечение значений свойств в JavaScript более сложной задачей, чем в нединамических языках, таких, как Java и C#. В Java, например, все свойства объекта определяются не изменяющейся после компиляции программы схемой объекта, их нельзя динамически добавлять или удалять (надо отметить, что в C# есть динамический тип, но тут мы можем не обращать на это внимание). Как результат, значения свойств (или указатели на эти свойства) могут быть сохранены, с фиксированным смещением, в виде непрерывного буфера в памяти. Шаг смещения можно легко определить, основываясь на типе свойства, в то время как в JavaScript это невозможно, так как тип свойства может меняться в процессе выполнения программы.
Так как использование словарей для выяснения адресов свойств объекта в памяти очень неэффективно, V8 использует вместо этого другой метод: скрытые классы. Скрытые классы похожи на обычные классы в типичном объектно-ориентированном языке программирования, вроде Java, за исключением того, что создаются они во время выполнения программы. Посмотрим, как всё это работает, на следующем примере:
function Point(x, y) < this.x = x; this.y = y; >var p1 = new Point(1, 2);

Когда происходит вызов new Point(1, 2) , V8 создаёт скрытый класс C0 .
Первый скрытый класс С0
Пока, ещё до выполнения конструктора, у объекта Point нет свойств, поэтому класс C0 пуст.
Как только будет выполнена первая команда в функции Point , V8 создаст второй скрытый класс, C1 , который основан на C0 . C1 описывает место в памяти (относительно указателя объекта), где можно найти свойство x . В данном случае свойство x хранится по смещению 0, что означает, что если рассматривать объект Point в памяти как непрерывный буфер, первое смещение соответствует свойству x . Кроме того, V8 добавит в класс C0 сведения о переходе к классу C1 , где указывается, что если к объекту Point будет добавлено свойство x , скрытый класс нужно изменить с C0 на C1 . Скрытый класс для объекта Point , как показано на рисунке ниже, теперь стал классом С1 .

Каждый раз, когда к объекту добавляется новое свойство, в старый скрытый класс добавляются сведения о переходе к новому скрытому классу. Переходы между скрытыми классами важны, так как они позволяют объектам, которые создаются одинаково, иметь одни и те же скрытые классы. Если два объекта имеют общий скрытый класс и к ним добавляется одно и то же свойство, переходы обеспечат то, что оба объекта получат одинаковый новый скрытый класс и весь оптимизированный код, который идёт вместе с ним.
Этот процесс повторяется при выполнении команды this.y = y (опять же, делается это внутри функции Point , после вышеописанной команды по добавлению свойства x ).
Тут создаётся новый скрытый класс, C2 , а в класс C1 добавляются сведения о переходе, где указывается, что если к объекту Point добавляется свойство y (при этом речь идёт об объекте, который уже содержит свойство x ), тогда скрытый класс объекта должен измениться на C2 .

Переход к использованию класса C2 после добавления к объекту свойства y
Переходы между скрытыми классами зависят от порядка, в котором к объекту добавляются свойства. Взгляните на этот пример кода:
function Point(x, y) < this.x = x; this.y = y; >var p1 = new Point(1, 2); p1.a = 5; p1.b = 6; var p2 = new Point(3, 4); p2.b = 7; p2.a = 8;
В подобной ситуации можно предположить, что у объектов p1 и p2 будет один и тот же скрытый класс и одно и то же дерево переходов скрытых классов. Однако, на самом деле это не так. В объект p1 первым добавляется свойство a , а затем — свойство b . В объект p2 сначала добавляют свойство b , а затем — a . В результате объекты p1 и p2 будут иметь различные скрытые классы — результат различных путей переходов между скрытыми классами. В подобных случаях гораздо лучше инициализировать динамические свойства в одном и том же порядке для того, чтобы скрытые классы могли быть использованы повторно.
Встроенные кэши
V8 использует и другую технику для оптимизации выполнения динамически типизированных языков, называемую встроенным кэшем вызовов. Встроенное кэширование основано на наблюдении, которое заключается в том, что повторяющиеся обращения к одному и тому же методу имеют тенденцию происходить с использованием объектов одного типа. Более подробно об этом можно почитать здесь. Если у вас нет времени слишком в это углубляться, читая вышеупомянутый материал, здесь мы изложим концепцию встроенного кэширования буквально в двух словах.
Итак, как же всё это работает? V8 поддерживает кэш типов объектов, которые мы передали в качестве параметра недавно вызванным методам, и использует эту информацию для того, чтобы сделать предположение о типах объектов, которые будут переданы как параметры в будущем. Если V8 смог сделать правильное предположение о типе объекта, который будет передан методу, он может пропустить процесс выяснения того, как получать доступ к свойствам объекта, а, вместо этого, использовать сохранённую информацию из предыдущих обращений к скрытому классу объекта.
Как связаны концепции скрытых классов и встроенных кэшей вызовов? Когда метод вызывается для некоего объекта, движок V8 должен обратиться к скрытому классу этого объекта для того, чтобы определить смещение для доступа к конкретному свойству. После двух успешных обращений одного и того же метода к одному и тому же скрытому классу, V8 опускает операцию обращения к скрытому классу и просто добавляет сведения о смещении свойства к самому указателю объекта. Выполняя вызовов этого метода в будущем, V8 предполагает , что скрытый класс не менялся и идёт прямо по адресу памяти для конкретного свойства, используя смещение, сохранённое после предыдущих обращений к скрытому классу. Это очень сильно увеличивает скорость выполнения кода.
Встроенное кэширование вызовов, кроме того, является причиной того, почему так важно, чтобы объекты одного и того же типа использовали общие скрытые классы. Если вы создаёте два объекта одинакового типа, но с разными скрытыми классами (как сделано в примере выше), V8 не сможет использовать встроенное кэширование, так как, даже хотя объекты имеют один и тот же тип, в соответствующих им скрытых классах назначено разное смещение их свойствам.

Перед нами объекты одного типа, но их свойства a и b были созданы в разном порядке и имеют разное смещение
Компиляция в машинный код
Как только граф Hydrogen оптимизирован, Crankshaft переводит его в низкоуровневое представление, которое называется Lithium. Большинство реализаций Lithium зависимо от архитектуры системы. На этом уровне, например, происходит выделение регистров.
В итоге Lithium-представление компилируется в машинный код. Затем происходит то, что называется замещением в стеке (on-stack replacement, OSR). Перед компиляцией и оптимизацией методов, в которых программа тратит много времени, нужно будет поработать с их неоптимизированными вариантами. Затем, не прерывая работу, V8 трансформирует контекст (стек, регистры) таким образом, чтобы можно было переключиться на оптимизированную версию кода. Это очень сложная задача, учитывая то, что помимо других оптимизаций, V8 изначально выполняет встраивание кода. V8 — не единственный движок, способный это сделать.
Как быть, если оптимизация оказалась неудачной? От этого есть защита — так называемая деоптимизация. Она направлена на обратную трансформацию, возвращающую систему к использованию неоптимизированного кода в том случае, если предположения, сделанные движком и положенные в основу оптимизации, больше не соответствуют действительности.
Сборка мусора
Для сборки мусора V8 использует традиционный генеалогический подход «пометь и выброси» (mark-and-sweep) для маркировки и очистки предыдущих поколений кода. Фаза маркировки предполагает остановку выполнения JavaScript. Для того, чтобы контролировать нагрузку на систему, создаваемую сборщиком мусора и сделать выполнение кода более стабильным, V8 использует инкрементный алгоритм маркирования: вместо того, чтобы обходить всю кучу, он пытается пометить всё, что сможет, обходя лишь часть кучи. Затем нормальное выполнение кода возобновляется. Следующий проход сборщика мусора по куче начинается там, где закончился предыдущий. Это позволяет добиться очень коротких пауз в ходе обычного выполнения кода. Как уже было сказано, фазой очистки памяти занимаются отдельные потоки.
Ignition и TurboFan
С выходом в этом году V8 версии 5.9. был представлен и новый конвейер выполнения кода. Этот конвейер позволяет достичь ещё большего улучшения производительности и значительной экономии памяти, причём, не в тестах, а в реальных JavaScript-приложениях.
Новая система построена на базе интерпретатора Ingnition и новейшего оптимизирующего компилятора TurboFan. Подробности об этих новых механизмах V8 можно почитать в этом материале.
С выходом V8 5.9 full-codegen и Crankshaft (технологии, которые использовались в V8 с 2010-го года) больше применяться не будут. Команда V8 развивает новые средства, стараясь не отстать от новых возможностей JavaScript и внедрить оптимизации, необходимые для поддержки этих возможностей. Переход на новые технологии и отказ от поддержки старых механизмов означает развитие V8 в сторону более простой и хорошо управляемой архитектуры.

Улучшения в тестах производительности для браузерного и серверного вариантов использования JS
Эти улучшения — лишь начало. Новый конвейер выполнения кода на основе Ignition и TurboFan открывает путь к дальнейшим оптимизациям, которые улучшат производительность JavaScript и сделают V8 экономичнее.
Мы рассмотрели некоторые особенности V8, а теперь приведём несколько советов по оптимизации кода. На самом деле, кстати, всё это вполне можно вывести из того, о чём мы говорили выше.
Подходы к оптимизации JavaScript-кода для V8
- Порядок свойств объектов. Всегда инициализируйте свойства объектов в одном и том же порядке. Нужно это для того, чтобы одинаковые объекты использовали одни и те же скрытые классы, и, как следствие, оптимизированный код.
- Динамические свойства. Добавление свойств к объектам после создания экземпляра объекта приведёт к изменению скрытого класса и к замедлению методов, которые были оптимизированы для скрытого класса, используемого объектами ранее. Вместо добавления свойств динамически, назначайте их в конструкторе объекта.
- Методы. Код, который несколько раз вызывает один и тот же метод, будет выполняться быстрее, чем код, который вызывает несколько разных методов по одному разу (из-за встроенных кэшей).
- Массивы. Избегайте разреженных массивов, ключи которых не являются последовательными числами. Разреженный массив, то есть массив, некоторые из элементов которого отсутствуют, будет обрабатываться системой как хэш-таблица. Для доступа к элементам такого массива требуется больше вычислительных ресурсов. Кроме того, постарайтесь избежать заблаговременного выделения памяти под большие массивы. Лучше, если их размер будет увеличиваться по мере надобности. И, наконец, не удаляйте элементы в массивах. Из-за этого они превращаются в разреженные массивы.
- Числа. V8 представляет числа и указатели на объекты, используя 32 бита. Он задействует один бит для того, чтобы определить, является ли некое 32-битное значение указателем на объект (флаг — 1), или целым числом (флаг — 0), которое называется маленьким целым числом (SMall Integer, SMI) из-за того, что его длина составляет 31 бит. Если для хранения числового значения требуется более 31 бита, V8 упакует число, превратив его в число двойной точности и создаст новый объект для того, чтобы поместить в него это число. Постарайтесь использовать 31-битные числа со знаком везде, где это возможно, для того, чтобы избежать ресурсоёмких операций упаковки чисел в JS-объекты.
Итоги
Мы, в SessionStack, стараемся следовать вышеизложенным принципам при написании JS-кода. Надеемся, немного разобравшись в том, как работают внутренние механизмы V8, и учтя то, что мы рассказали выше, вы сможете улучшить качество и производительность ваших программ.
Уважаемые читатели! Какими советами по оптимизации JS-кода можете поделиться вы?
- Блог компании RUVDS.com
- Веб-разработка
- JavaScript
JavaScript-движки: как они работают? От стека вызовов до промисов — (почти) всё, что вам нужно знать

Вы когда-нибудь задумывались, как браузеры читают и исполняют JavaScript-код? Это выглядит таинственно, но в этом посте вы можете получить представление, что же происходит под капотом.
Начнём наше путешествие в язык с экскурсии в удивительный мир JavaScript-движков.
Откройте консоль в Chrome и перейдите на вкладку Sources. Вы увидите несколько разделов, и один из самых интересных называется Call Stack (в Firefox вы увидите Call Stack, когда поставите брейкпоинт в коде):

Что такое Call Stack? Похоже, тут много чего происходит, даже ради исполнения пары строк кода. На самом деле JavaScript не поставляется в коробке с каждым браузером. Существует большой компонент, который компилирует и интерпретирует наш JavaScript-код — это JavaScript-движок. Самыми популярными являются V8, он используется в Google Chrome и Node.js, SpiderMonkey в Firefox, JavaScriptCore в Safari/WebKit.
Сегодня JavaScript-движки представляют собой прекрасные образцы программной инженерии, и будет практически невозможно рассказать обо всех аспектах. Однако основную работу по исполнению кода делают для нас лишь несколько компонентов движков: Call Stack (стек вызовов), Global Memory (глобальная память) и Execution Context (контекст исполнения). Готовы с ними познакомиться?
- JavaScript-движки и глобальная память
- JavaScript-движки: как они работают? Глобальный контекст исполнения и стек вызовов
- JavaScript является однопоточным, и другие забавные истории
- Асинхронный JavaScript, очередь обратных вызовов и цикл событий
- Callback hell и промисы ES6
- Создание и работа с JavaScript-промисами
- Обработка ошибок в ES6-промисах
- Комбинаторы ES6-промисов: Promise.all, Promise.allSettled, Promise.any и другие
- ES6-промисы и очередь микрозадач
- JavaScript-движки: как они работают? Асинхронная эволюция: от промисов до async/await
- JavaScript-движки: как они работают? Итоги
1. JavaScript-движки и глобальная память
Я говорил, что JavaScript является одновременно компилируемым и интерпретируемым языком. Хотите верьте, хотите нет, но на самом деле JavaScript-движки компилируют ваш код за микросекунды до его исполнения.
Волшебство какое-то, да? Это волшебство называется JIT (Just in time compilation). Она сама по себе является большой темой для обсуждения, даже книги будет мало, чтобы описать работу JIT. Но пока что мы пропустим теорию и сосредоточимся на фазе исполнения, которая не менее интересна.
Для начала посмотрите на этот код:
var num = 2; function pow(num)
Допустим, я спрошу вас, как этот код обрабатывается в браузере? Что вы ответите? Вы можете сказать: «браузер читает код» или «браузер исполняет код». В реальности всё не так просто. Во-первых, код считывает не браузер, а движок. JavaScript-движок считывает код, и как только он определяет первую строку, то кладёт пару ссылок в глобальную память.
Глобальная память (которую также называют кучей (heap)) — это область, в которой JavaScript-движок хранит переменные и объявления функций. И когда он прочитает приведённый выше код, то в глобальной памяти появятся два биндинга:

Даже если в примере содержится лишь переменная и функция, представьте, что ваш JavaScript-код исполняется в более крупной среде: в браузере или в Node.js. В таких средах есть много заранее определённых функций и переменных, которые называют глобальными. Поэтому глобальная память будет содержать гораздо больше данных, чем просто num и pow , имейте в виду.
В данный момент ничего не исполняется. Давайте теперь попробуем исполнить нашу функцию:
var num = 2; function pow(num) < return num * num; >pow(num);
Что произойдёт? А произойдёт кое-что интересное. При вызове функции JavaScript-движок выделит два раздела:
- Глобальный контекст исполнения (Global Execution Context)
- Стек вызовов (Call Stack)
2. JavaScript-движки: как они работают? Глобальный контекст исполнения и стек вызовов
Вы узнали, как JavaScript-движок читает переменные и объявления функций. Они попадают в глобальную память (кучу).
Но теперь мы исполняем JavaScript-функцию, и движок должен об этом позаботиться. Каким образом? У каждого JavaScript-движка есть ключевой компонент, который называется стек вызовов.
Это стековая структура данных: элементы могут добавляться в неё сверху, но они не могут исключаться из структуры, пока над ними есть другие элементы. Именно так устроены JavaScript-функции. При исполнении они не могут покинуть стек вызовов, если в нём присутствует другая функция. Обратите на это внимание, поскольку эта концепция помогает понять утверждение «JavaScript является однопоточным».
Но вернёмся к нашему примеру. При вызове функции движок отправляет её в стек вызовов:

Мне нравится представлять стек вызовов в виде стопки чипсов Pringles. Мы не можем съесть чипс снизу стопки, пока не съедим те, что лежат сверху. К счастью, наша функция является синхронной: это всего лишь умножение, которое быстро вычисляется.
В то же самое время движок размещает в памяти глобальный контекст исполнения, это глобальная среда, в которой исполняется JavaScript-код. Вот как это выглядит:

Представьте глобальный контекст исполнения в виде моря, в котором глобальные JavaScript-функции плавают, словно рыбы. Как мило! Но это лишь половина всей истории. Что, если наша функция имеет вложенные переменные или внутренние функции?
Даже в простом случае, как показано ниже, JavaScript-движок создаёт локальный контекст исполнения:
var num = 2; function pow(num) < var fixed = 89; return num * num; >pow(num);
Обратите внимание, что я добавил в функцию pow переменную fixed . В этом случае локальный контекст исполнения будет содержать раздел для fixed . Я не очень хорошо рисую маленькие прямоугольники внутри других маленьких маленьких прямоугольников, так что используйте своё воображение.
Рядом с pow появится локальный контекст исполнения, внутри зелёного раздела-прямоугольника, расположенного внутри глобального контекста исполнения. Представьте также, как для каждой вложенной функции внутри вложенной функции движок создаёт другие локальные контексты исполнения. Все эти разделы-прямоугольники появляются очень быстро! Как матрёшка!
Давайте теперь вернёмся к истории с однопоточностью. Что это означает?
3. JavaScript является однопоточным, и другие забавные истории
Мы говорим, что JavaScript является однопоточным, потому что наши функции обрабатывает лишь один стек вызовов. Напомню, что функции не могут покинуть стек вызовов, если исполнения ожидают другие функции.
Это не проблема, если мы работаем с синхронным кодом. К примеру, сложение двух чисел является синхронным и вычисляется за микросекунды. А что насчёт сетевых вызовов и других взаимодействий с внешним миром?
К счастью, JavaScript-движки спроектированы так, чтобы по умолчанию работать асинхронно. Даже если они могут исполнять только по одной функции за раз, более медленные функции могут исполняться внешней сущностью — в нашем случае это браузер. Об этом мы поговорим ниже.
В то же время вы знаете, что когда браузер загружает какой-то JavaScript-код, движок считывает этот код строка за строкой и выполняет следующие шаги:
- Помещает в глобальную память (кучу) переменные и объявления функций.
- Отправляет вызов каждой функции в стек вызовов.
- Создаёт глобальный контекст исполнения, в котором исполняются глобальные функции.
- Создаёт много маленьких локальных контекстов исполнения (если есть внутренние переменные или вложенные функции).
4. Асинхронный JavaScript, очередь обратных вызовов и цикл событий
Благодаря глобальной памяти, контексту исполнения и стеку вызовов синхронный JavaScript-код исполняется в наших браузерах. Но мы кое о чём забыли. Что происходит, если нужно исполнить какую-нибудь асинхронную функцию?
Под асинхронной функцией я подразумеваю каждое взаимодействие с внешним миром, для завершения которого может потребоваться какое-то время. Вызов REST API или таймера — асинхронны, потому что на их выполнение могут уйти секунды. Благодаря имеющимся в движке элементам мы можем обрабатывать такие функции без блокирования стека вызовов и браузера. Не забывайте, стек вызовов может исполнять одновременно только одну функцию, и даже одна блокирующая функция может буквально остановить браузер. К счастью, JavaScript-движки «умны», и с небольшой помощью браузера могут такие вещи отсортировывать.
Когда мы исполняем асинхронную функцию, браузер берёт её и выполняет для нас. Возьмём такой таймер:
setTimeout(callback, 10000); function callback()
Уверен, что хоть вы и видели setTimeout уже сотни раз, однако можете не знать, что эта функция не встроена в JavaScript. Вот так, когда JavaScript появился, в нём не было функции setTimeout . По сути, она является частью так называемых браузерных API, коллекции удобных инструментов, которые нам предоставляет браузер. Чудесно! Но что это означает на практике? Поскольку setTimeout относится к браузерным API, эта функция исполняется самим браузером (на мгновение она появляется в стеке вызовов, но сразу оттуда удаляется).
Через 10 секунд браузер берёт callback-функцию, которую мы ему передали, и кладёт её в очередь обратных вызовов. В данный момент в JavaScript-движке появилось ещё два раздела-прямоугольника. Посмотрите на этот код:
var num = 2; function pow(num) < return num * num; >pow(num); setTimeout(callback, 10000); function callback()
Теперь наша схема выглядит так:

setTimeout исполняется внутри контекста браузера. Через 10 секунд таймер запускается и callback-функция готова к исполнению. Но для начала она должна пройти через очередь обратных вызовов. Это структура данных в виде очереди, и, как свидетельствует её название, представляет собой упорядоченную очередь из функций.
Каждая асинхронная функция должна пройти через очередь обратных вызовов, прежде чем попасть в стек вызовов. Но кто отправляет функции дальше? Это делает компонент под названием цикл событий.
Пока что цикл событий занимается только одним: проверяет, пуст ли стек вызовов. Если в очереди обратных вызовов есть какая-нибудь функция и если стек вызовов свободен, тогда пора отправлять callback в стек вызовов.
После этого функция считается исполненной. Так выглядит общая схема обработки асинхронного и синхронного кода JavaScript-движком:

Допустим, callback() готова к исполнению. После завершения исполнения pow() стек вызовов освобождается и цикл событий отправляет в него callback() . И всё! Хотя я немного всё упростил, если вы поняли приведённую выше схему, то можете понять и весь JavaScript.
Помните: браузерные API, очередь обратных вызовов и цикл событий являются столпами асинхронного JavaScript.
И если интересно, можете посмотреть любопытное видео «What the heck is the event loop anyway» Филипа Робертса. Это одно из лучших объяснений цикла событий.
Но мы ещё не закончили с темой асинхронного JavaScript. В следующих главах мы рассмотрим ES6-промисы.
5. Callback hell и ES6-промисы
Callback-функции используются в JavaScript везде, и в синхронном, и в асинхронном коде. Рассмотрим этот метод:
function mapper(element) < return element * 2; >[1, 2, 3, 4, 5].map(mapper);
mapper — это callback-функция, которая передаётся внутри map . Приведённый код является синхронным. А теперь рассмотрим этот интервал:
function runMeEvery() < console.log('Ran!'); >setInterval(runMeEvery, 5000);
Этот код асинхронный, поскольку внутри setInterval мы передаём обратный вызов runMeEvery . Обратные вызовы применяются по всему JavaScript, так что у нас годами существует проблема, получившая название «callback hell» — «ад обратных вызовов».
Термин Callback hell в JavaScript применяют к «стилю» программирования, при котором callback’и вкладывают в другие callback’и, которые вложены в другие callback’и… Из-за асинхронной природы JavaScript-программисты уже давно попадают в эту ловушку.
Если честно, я никогда не создавал большие пирамиды callback’ов. Возможно, потому что я ценю читабельный код и всегда стараюсь придерживаться его принципов. Если вы попали в callback hell, это говорит о том, что ваша функция делает слишком много.
Я не буду подробно говорить о callback hell, если вам интересно, то сходите на сайт callbackhell.com, там эта проблема подробно исследована и предложены разные решения. А мы поговорим о ES6-промисах. Это аддон к JavaScript, призванное решить проблему ада обратных вызовов. Но что такое «промисы»?
Промис в JavaScript — это представление будущего события. Промис может завершиться успешно, или на жаргоне программистов промис будет «разрешён» (resolved, исполнен). Но если промис завершается с ошибкой, то мы говорим, что он в состоянии «отклонён» (rejected). Также у промисов есть состояние по умолчанию: каждый новый промис начинается в состоянии «ожидания решения» (pending). Можно ли создать собственный промис? Да. Об этом мы поговорим в следующей главе.
6. Создание и работа с JavaScript-промисами
Для создания нового промиса нужно вызвать конструктор, передав в него callback-функцию. Она может принимать только два параметра: resolve и reject . Давайте создадим новый промис, который будет разрешён через 5 секунд (можете протестировать примеры в браузерной консоли):
const myPromise = new Promise(function(resolve)< setTimeout(function()< resolve() >, 5000) >);
Как видите, resolve — это функция, которую мы вызываем, чтобы промис успешно завершился. А reject создаст отклонённый промис:
const myPromise = new Promise(function(resolve, reject)< setTimeout(function()< reject() >, 5000) >);
Обратите внимание, что вы можете игнорировать reject , потому что это второй параметр. Но если вы намерены воспользоваться reject , то не сможете проигнорировать resolve . То есть следующий код не будет работать и закончится разрешённым промисом:
// Can't omit resolve ! const myPromise = new Promise(function(reject)< setTimeout(function()< reject() >, 5000) >);
Сейчас промисы не выглядят такими полезными, верно? Эти примеры ничего не выводят для пользователя. Давайте кое-что добавим. И разрешённые, от отклонённые промисы могут возвращать данные. Например:
const myPromise = new Promise(function(resolve) < resolve([< name: "Chris" >]); >);
Но мы всё ещё ничего не видим. Для извлечения данных из промиса вам нужно связать промис с методом then . Он берёт callback (какая ирония!), который получает актуальные данные:
const myPromise = new Promise(function(resolve, reject) < resolve([< name: "Chris" >]); >); myPromise.then(function(data) < console.log(data); >);
Как JavaScript-разработчик и потребитель чужого кода вы по большей части взаимодействуете с внешними промисами. Создатели библиотек чаще всего обёртывают legacy-код в конструктор промисов, таким образом:
const shinyNewUtil = new Promise(function(resolve, reject) < // do stuff and resolve // or reject >);
И при необходимости мы также можем создать и разрешить промис, вызвав Promise.resolve() :
Promise.resolve(< msg: 'Resolve!'>) .then(msg => console.log(msg));
Итак, напомню: промисы в JavaScript — это закладка на событие, которое произойдёт в будущем. Событие начинается в состоянии «ожидание решения», и может быть успешным (разрешённым, исполненным) или неуспешным (отклонённым). Промис может возвращать данные, которые можно извлечь, прикрепив к промису then . В следующей главе мы обсудим, как работать с ошибками, приходящими из промисов.
7. Обработка ошибок в ES6-промисах
Обрабатывать ошибки в JavaScript всегда было просто, как минимум в синхронном коде. Взгляните на пример:
function makeAnError() < throw Error("Sorry mate!"); >try < makeAnError(); >catch (error)
Catching the error! Error: Sorry mate!
Как и ожидалась, ошибка попала в блок catch . Теперь попробуем асинхронную функцию:
function makeAnError() < throw Error("Sorry mate!"); >try < setTimeout(makeAnError, 5000); >catch (error)
Этот код является асинхронным из-за setTimeout . Что будет, если мы его исполним?
throw Error("Sorry mate!"); ^ Error: Sorry mate! at Timeout.makeAnError [as _onTimeout] (/home/valentino/Code/piccolo-javascript/async.js:2:9)
Теперь результат другой. Ошибка не была поймана блоком catch , а свободно поднялась выше по стеку. Причина в том, что try/catch работает только с синхронным кодом. Если хотите узнать больше, то эта проблема подробно рассмотрена здесь.
К счастью, с промисами мы можем обрабатывать асинхронные ошибки, словно они синхронные. В прошлой главе я говорил, что вызов reject приводит к отклонению промиса:
const myPromise = new Promise(function(resolve, reject) < reject('Errored, sorry!'); >);
В этом случае мы можем обрабатывать ошибки с помощью обработчика catch , дёргая (опять) обратный вызов:
const myPromise = new Promise(function(resolve, reject) < reject('Errored, sorry!'); >); myPromise.catch(err => console.log(err));
Кроме того, чтобы для создания и отклонения промиса в нужном месте можно вызывать Promise.reject() :
Promise.reject().catch(err => console.log(err));
Напомню: обработчик then исполняется, когда промис выполнен, а обработчик catch выполняется для отклонённых промисов. Но это ещё не конец истории. Ниже мы увидим, как async/await замечательно работают с try/catch .
8. Комбинаторы ES6-промисов: Promise.all, Promise.allSettled, Promise.any и другие
Промисы не предназначены для работы по одиночке. Promise API предлагает ряд методов для комбинирования промисов. Один из самых полезных — Promise.all, он берёт массив из промисов и возвращает один промис. Только проблема в том, что Promise.all отклоняется, если отклонен хотя бы один промис в массиве.
Promise.race разрешает или отклоняет, как только один из промисов в массиве получает соответствующий статус.
В более свежих версиях V8 также будут внедрены два новых комбинатора: Promise.allSettled и Promise.any . Promise.any пока на ранней стадии предложенной функциональности, на момент написания статьи не поддерживается. Однако, в теории, он сможет сигнализировать, был ли исполнен какой-либо промис. Отличие от Promise.race в том, что Promise.any не отклоняется, даже если отклонён один из промисов.
Promise.allSettled ещё интереснее. Он тоже берёт массив промисов, но не «коротит», если один из промисов отклоняется. Он полезен, когда нужно проверить, все ли промисы в массиве перешли в какую-то стадию, вне зависимости от наличия отклонённых промисов. Его можно считать противоположностью Promise.all .
9. ES6-промисы и очередь микрозадач
Если помните из предыдущей главы, каждая асинхронная callback-функция в JavaScript оказывается в очереди обратных вызовов, прежде чем попадает в стек вызовов. Но у callback-функций, переданных в промис, иная судьба: они обрабатываются очередью микрозадач (Microtask Queue), а не очередью задач.
И здесь вам нужно быть внимательными: очередь микрозадач предшествует очереди вызовов. Обратные вызовы из очереди микрозадач имеют приоритет, когда цикл событий проверяет, готовы ли новые callback’и перейти в стек вызовов.
Подробнее эта механика описана Джейком Арчибальдом в Tasks, microtasks, queues and schedules, замечательное чтиво.
10. JavaScript-движки: как они работают? Асинхронная эволюция: от промисов до async/await
JavaScript быстро развивается и мы каждый год получаем постоянные улучшения. Промисы выглядели как финал, но с ECMAScript 2017 (ES8) появился новый синтаксис: async/await .
async/await — всего лишь стилистическое улучшение, которое мы называем синтаксическим сахаром. async/await никак не меняет JavaScript (не забывайте, язык должен быть обратно совместим со старыми браузерами и не должен ломать существующий код). Это лишь новый способ написания асинхронного кода на основе промисов. Рассмотрим пример. Выше мы уже сохранили промис в соответствующем then :
const myPromise = new Promise(function(resolve, reject) < resolve([< name: "Chris" >]); >); myPromise.then((data) => console.log(data))
Теперь с помощью async/await мы можем обработать асинхронный код так, чтобы для читающего наш листинг код выглядел синхронным. Вместо применения then мы можем обернуть промис в функцию, помеченную как async , и затем будем ожидать ( await ) результат:
const myPromise = new Promise(function(resolve, reject) < resolve([< name: "Chris" >]); >); async function getData() < const data = await myPromise; console.log(data); >getData();
Выглядит здраво, верно? Забавно, что async-функция всегда возвращает промис, и никто не может ей в этом помешать:
async function getData() < const data = await myPromise; return data; >getData().then(data => console.log(data));
А что насчёт ошибок? Одно из преимуществ async/await в том, что эта конструкция может позволить нам воспользоваться try/catch . Почитайте введение в обработку ошибок в async-функциях и их тестирование.
Давайте снова взглянем на промис, в котором мы обрабатываем ошибки с помощью обработчика catch :
const myPromise = new Promise(function(resolve, reject) < reject('Errored, sorry!'); >); myPromise.catch(err => console.log(err));
С асинхронными функциями мы можем отрефакторить вот так:
async function getData() < try < const data = await myPromise; console.log(data); // or return the data with return data >catch (error) < console.log(error); >> getData();
Однако ещё не все перешли на этот стиль. try/catch может усложнить ваш код. При этом нужно учитывать ещё кое-что. Посмотрите, как в этом коде возникает ошибка внутри блока try :
async function getData() < try < if (true) < throw Error("Catch me if you can"); >> catch (err) < console.log(err.message); >> getData() .then(() => console.log("I will run no matter what!")) .catch(() => console.log("Catching err"));
Что насчёт двух строк, которые выводятся в консоли? Не забывайте, что try/catch — синхронная конструкция, а наша асинхронная функция генерирует промис. Они идут по двум разным путям, словно поезда. Но они никогда не встретятся! Поэтому ошибка, которую подняла throw , никогда не активирует обработчик catch в getData() . Исполнение этого кода приведёт к тому, что сначала появится надпись «Catch me if you can», а за ней «I will run no matter what!».
В реальном мире нам не нужно, чтобы throw запускал обработчик then . Решить это можно, скажем, возвращая Promise.reject() из функции:
async function getData() < try < if (true) < return Promise.reject("Catch me if you can"); >> catch (err) < console.log(err.message); >> Now the error will be handled as expected: getData() .then(() => console.log("I will NOT run no matter what!")) .catch(() => console.log("Catching err")); "Catching err" // output
Помимо этого async/await выглядит лучшим способом структурирования асинхронного кода в JavaScript. Мы лучше управляем обработкой ошибок и код выглядит чище.
В любом случае, я не рекомендую рефакторить весь ваш JS-код под async/await . Обсудите это с командой. Но если вы работаете самостоятельно, то выбор между чистыми промисами и async/await — лишь дело вкуса.
11. JavaScript-движки: как они работают? Итоги
JavaScript — это скриптовый язык для веба, он сначала компилируется, а затем интерпретируется движком. Самые популярные JS-движки: V8, применяется в Google Chrome и Node.js; SpiderMonkey, разработан для Firefox; JavaScriptCore, используется в Safari.
JavaScript-движки имеют много «движущихся» частей: стек вызовов, глобальная память, цикл событий, очередь обратных вызовов. Все эти части идеально работают вместе, обеспечивая обработку синхронного и асинхронного кода.
JavaScript-движки являются однопоточными, то есть для исполнения функций применяется единственный стек вызовов. Это ограничение лежит в основе асинхронной природы JavaScript: все операции, для выполнения которых требуется какое-то время, должны управляться внешней сущностью (например, браузером) или функцией обратного вызова.
Для упрощения работы асинхронного кода в ECMAScript 2015 были внедрены промисы. Промис — это асинхронный объект, используемый для представления успешности или неуспешности любой асинхронной операции. Но улучшения на этом не прекратились. В 2017-м появились async/await : стилистическое улучшение для промисов, позволяющее писать асинхронный код, как если бы он был синхронным.
- Блог компании VK
- JavaScript
- Программирование
- Node.JS
- Браузеры
Движок JavaScript: что внутри

Чтобы разобраться в том, как работает механизм обработки кода (иначе говоря, движок JavaScript), надо понять, что происходит при выполнении кода. Такие знания помогают программистам писать лучший, более быстрый и умный код.
Движки JavaScript — это не что иное, как программы, преобразующие код на JavaScript в код более низкого уровня, который компьютер сможет понять. Эти движки встроены в браузеры и веб-серверы (Node.js), что даёт возможность выполнять код и осуществлять компиляцию во время выполнения.
Разве JavaScript — это не интерпретируемый язык?
Краткий ответ: это зависит от реализации. Обычно JavaScript относят к интерпретируемым языкам, хотя вообще-то он компилируется. Современные компиляторы JavaScript фактически выполняют JIT-компиляцию, т.е. компиляцию «на лету», которая осуществляется во время работы программы.
Движок
Существует множество разных движков. У каждого из них внутри есть что-то вроде конвейера с интерпретатором и конвейера с оптимизатором и компилятором. Интерпретатор генерирует байт-код, а оптимизатор выдаёт оптимизированный машинный код. Далее в статье в качестве примера будет использоваться движок V8.
V8 — это высокопроизводительный движок от Google с открытым исходным кодом JavaScript и WebAssembly, написанный на языке C++.Он используется в Chrome, Node.js и других платформах и реализует ECMAScript и WebAssembly (см.v8.dev).
Что внутри движка
Всякий раз, когда JavaScript-код отправляется в движок V8, он проходит ряд этапов для отображения console.log:

Парсер
Движок выполняет то, что мы называем лексическим анализом. Это первое, что происходит с файлом JavaScript при попадании в движок. Код разбивается на части, называемые токенами, для выявления их назначения, после чего мы узнаём, что код пытается сделать.
Абстрактное синтаксическое дерево (AST)
На основе этих токенов создаётся то, что мы называем AST. Синтаксическое дерево — это древовидное представление синтаксической структуры кода JavaScript, и мы можем использовать этот инструмент для анализа преобразования кода AST.
Интерпретатор
Интерпретатор читает файлы JavaScript построчно и преобразовывает их на ходу (во время работы программы, не прерывая её выполнение). На основе сгенерированного кода AST интерпретатор начинает быстро создавать байт-код. Никаких оптимизаций здесь не выполняется, так что байт-код этот неоптимизированный.
Байт-код не является таким низкоуровневым, как машинный код, но всё же может быть интерпретирован движком JavaScript для выполнения кода.
Профайлер
Профайлер отвечает за проверку кода. Он вызывает специальное средство контроля, которое отслеживает код и наблюдает за ходом его выполнения, обращая наше внимание на то, как можно оптимизировать код. Выдаёт, например, информацию о том, сколько раз код запускался, какие типы используются и как мы можем его оптимизировать?
Так что, если профайлер находит часть кода, которую можно оптимизировать, он передаёт этот код JIT-компилятору, чтобы он мог быть скомпилирован и выполнялся быстрее. Рассмотрим эти конвейеры с интерпретатором и компилятором более подробно.
Оптимизирующий компилятор
Задача оптимизирующего компилятора — определить, что делает программа, подлежащая оптимизации, и создать из неё оптимизированную программу, выполняющую всё то же самое, только быстрее.
Он не преобразует файлы на лету. Он делает свою работу заранее, создавая преобразование только что написанного кода и компилируя его в язык, понятный для компьютера.

Деоптимизация
Оптимизирующий компилятор на основе имеющихся у него данных профилирования делает определённые предположения, а затем выдаёт высокооптимизированный машинный код. Но вполне возможно, что в какой-то момент одно из предположений окажется неверным. И тогда оптимизирующий компилятор может деоптимизировать код.
Объектная модель JavaScript
JavaScript — это динамический язык программирования, а это подразумевает, что свойства могут легко добавляться или удаляться из объекта после его создания. Для написания более лучшего кода надо понимать, как JavaScript определяет объекты и как движок работает с объектами и свойствами.

- Enumerable → определяет, перечисляется ли свойство в циклах for — in .
- Value → само значение.
- Writable → определяет, можно ли свойство изменить.
- Configurable → определяет, можно ли удалить свойство.
Оптимизация доступа к свойству
В динамических языках, таких как JavaScript, для доступа к свойствам требуется ряд инструкций. Поэтому почти в каждом движке имеется оптимизация для ускорения этого доступа. Такая оптимизация в V8 реализуется скрытыми классами: V8 присоединяет скрытый класс к каждому отдельному объекту. Целью скрытых классов является оптимизация времени доступа к свойствам.
Скрытые классы работают аналогично фиксированным макетам (классам) объектов, используемым в таких языках, как Java (за исключением того, что они создаются во время выполнения). Магия здесь в том, что несколько объектов могут иметь один и тот же скрытый класс, поэтому необходимы только минимальные ресурсы, а код становится быстрее:

Выгода становится очевидной, когда объектов много. Ведь пока у них один и тот же скрытый класс, информацию приходится сохранять только один раз независимо от того, сколько всего имеется объектов!
Тема оптимизации очень обширна и может стать предметом обсуждения отдельной статьи.
Встроенные кэши
Цель встроенного кэширования в том, чтобы ускорить привязку метода времени выполнения. Происходит это за счёт запоминания результатов поиска предыдущего метода непосредственно в месте вызова.Встроенное кэширование особенно полезно для динамически типизированных языков, где большинство, если не все привязки методов происходят во время выполнения и где виртуальные таблицы методов часто не могут быть использованы.
Основная причина существования скрытых классов — концепция встроенных кэшей. Движки JavaScript используют встроенные кэши для запоминания информации о том, где найти свойства в объектах. Поэтому нет необходимости повторять дорогостоящий поиск свойств при каждом доступе к ним. Зачем каждый раз искать свойства, когда со встроенными кэшами это значительно быстрее?

Выводы
- инициализировать объекты лучше всегда одним и тем же способом, чтобы скрытые классы у них не были разными;
- с атрибутами свойств элементов массива надо быть осторожным, чтобы они могли аккуратно сохраняться, а работа с ними была эффективной.
- Как работает новый await верхнего уровня в JavaScript
- Чистый код JavaScript — Вертикальное форматирование
- Что значит this в JavaSсript?
Что такое Node.js и для чего он нужен
Браузеры постоянно совершенствуются, дорабатываются. Обычный пользователь при работе в сети этих изменений может даже не заметить, но для разработчиков они очень критичны. Для создания приложений на HTML5 приходится использовать новые инструменты и технологии. Одним из самых популярных IT-продуктов является Node.js.
Node.js: что это?
Node.js – это кроссплатформенная среда для разработки клиентских приложений, в основе которой лежит язык программирования JavaScript.
У Node.js открытый код, а построен он на движке Chrome V8, что позволяет переводить JavaScript в машинный код. С помощью этого инструмента можно запускать написанные программы из командной строки прямо на компьютере. Есть возможность установить его на сервер.
Также платформу можно назвать событийно-ориентированным I/O фреймворком. Node.js реагирует на действия пользователей, например, в чатах и играх. А технические особенности делают его удобным и легковесным.
Для чего нужен node js
Представьте, что вы написали браузерное приложение. Например, калькулятор для сайта, где пользователи могут рассчитать сумму заказа. И всё прекрасно работало, сайт был функциональным и удобным. Но потом у компании появился телеграм-канал, с которым пришлось интегрировать софт. Для этого нужно перенести проект на более мощный сервер. Раньше в такой ситуации нужно было писать код заново на серверном языке. Но Node.JS позволяет просто скопировать и перенести его, изменив только ввод-вывод.
Он используется для разнообразных сервисов, где происходит постоянный обмен информацией с пользователями. Это чаты, соцсети, сервисы совместной работы над проектом, онлайн-редакторы.
А ещё Node.js умеет обрабатывать множество запросов сразу. Поэтому он используется для разработки ПО «умных» вещей, у которых есть дистанционное управление и дополнительные функции.
Кроссплатформенная среда применяется широкого спектра 5 задач.
- WebSocket servers. Очень удобно разворачивать WebSosket или стриминг-приложения, так как наблюдается высокая скорость отклика.
- Клиент для быстрой загрузки файлов. В том числе можно загружать несколько больших файлов одновременно.
- Потоковая передача данных. Концепция обратного вызова позволяет получать результаты из нескольких сторонних API.
- Рекламный сервер. С помощью Node.JS можно обеспечить быструю загрузку отдельных изображений или видеороликов.
- ПО для фондовой биржи. Обмениваться данными можно в режиме реального времени.
Можно сказать Node.js открывает для написанного на JavaScript кода доступ к глобальным объектам, обращается к жесткому диску, базам данных и Сети. Поэтому с его помощью становится возможным написание любых приложений, в том числе видеоигр или веб-серверов.
Чем хорош node js
У этого продукта есть 7 основных особенностей, которые делают его функциональным, а потому востребованным.
- Асинхронный ввод-вывод. Благодаря ему удаётся управлять событиями, обрабатывая одновременно большое количество запросов. То есть применяется событийно-ориентированный подход, когда выполнение программ определяется действиями пользователей, поступлением сетевого пакета, оповещением из другой программы и т.д. Все эти операции могут протекать параллельно. Это удобно, когда в коде есть условие – если произойдёт такое событие, запусти такую-то функцию.
- Собственный пакет модулей, который обеспечивает предсказуемую сборку при создании сложных проектов.
- Высокая скорость. Написанный в среде Node.js код работает быстрее, чем код на компилируемых или интерпретируемых языках.
- Возможность использовать частей кода на серверной и клиентской стороне.
- Возможность передавать объёмные файлы.
- Удобство использования. JavaScript считается самым простым для изучения языком программирования. Поэтому Node.JS актуален для начинающих разработчиков.
- Активное и живое сообщество, которое поможет найти решения для самых разнообразных задач. Сейчас в библиотеке более 650 000 опенсорсных пакетов.
Недостатки у Node.js тоже есть. Например, очень неудобно работать с реляционными базами данных. Также решение не подходит для задач, для которых необходимы большие затраты процессора.
Платформа не даёт возможности масштабирования для использования преимуществ многоядерности. А ещё нужно учитывать, что при обратном вызове создаются дополнительные вложенные обратные вызовы.
Как установить Node.js
Скачать последнюю версию Node.JS можно бесплатно с официального сайта разработчика.
Запустите установочный пакет под учётной записью администратора. По умолчанию вместе со средой разработки будет установлен пакетный менеджер npm.
Механизм установки стандартный, нажимайте «Далее», чтобы перейти к следующему этапу. Будьте внимательны, по умолчанию устанавливается менеджер Node.js runtime, но нам нужно выбрать npm, как в примере ниже.
(1).png)
На всякий случай проверим, успешно ли выполнена установка. Для этого запустим командную строку «Нод».
(1).png)
Вы увидите Command Prompt. Здесь нужно ввести две команды.
node -v (Для проверки версии Node).
npm -v (Для проверки версии NPM).
Система покажет, какие версии установлены. Если ваш результат отличается от данных в примере, ничего страшного – версии часто обновляются.
(1).png)
Как пользоваться Node.js
Управление происходит через консоль. Соответственно, пользователю нужно вводить команды. Их синтаксис соответствует браузерному JavaScript.
Список команд очень длинный, поэтому учить наизусть их не нужно, всегда можно воспользоваться подсказкой.
(1).png)
Попробуем создать первое приложение. В качестве примера рассмотрим веб-сервер. Его код выглядит так.
const http = require(‘http’)
const hostname = ‘127.0.0.1’
const port = 3000
const server = http.createServer((req, res) => {
console.log(`Server running at http://${hostname}:${port}/`)
Этот код нужно сохранить в файле. Для этого вводим команду.
Готово. Теперь при указании адреса сервера в браузере на экране появится надпись “Hello World”.
Фреймворки и вспомогательные инструменты для Node.js
Node.js был выпущен в 2009 году. С того момента появилось внушительное количество библиотек, которые значительно упрощают работу программистов. В том числе существуют три типа фреймворков, которые выполняют функцию каркаса для написания программы.
MVC-фреймворки имеют собственный шаблон проектирования. Они состоят из трёх элементов: модели, представления и контроллеры. Такой формат помогает быстро выполнить несложные задачи.
Полностекевые MVC-фреймворки применяются для веб-приложений в realtime-режиме, а также при разработке фронтенда, бэкенда. В них можно найти большое число блоков, библиотек и других компонентов.
REST API предназначены для оперативной разработки приложений с помощью готового REST API.
Рассмотрим наиболее известные решения.
- Express. Простой, но функциональный инструмент для веб-серверов. Преимуществом считается минималистический подход, то есть ориентир на базовые серверные механизмы.
- Meteor. Актуален, когда код применяется на клиенте и сервере. Очень удобен в мобильной разработке.
- Koa. Решение от создателей Express, поэтому в приоритете тоже лёгкость и простота. Но его особенностью стали изменения, которые несовместимы с некоторыми механизмами Express.
- Next.js. Подходит для серверного рендеринга React-приложений.
- Micro. Решения для асинхронных HTTP-микросервисов.
- Socket.io. Пригодится при разработке сетевых приложений в режиме реального времени.
Подведём итоги
Node.js используется в качестве функционального конструктора приложений. Благодаря ему достаточно одного языка программирования для разработки веб-приложений, вместо нескольких разных для фронтенда и бэкенда. Можно сказать, что он эта среда основана на принципе «JavaScript для всего». Поэтому она так популярна среди разработчиков, независимо от особенностей их проектов.
Ещё одним преимуществом продукта является открытый код. Существует множество библиотек и фреймворков, которые ускоряет процесс разработки. При этом технология постоянно развивается, появляются эффективные инструменты для отладки, трейсинга и решения других задач. Поэтому, Node.js пока сохраняет актуальность, принося пользу начинающим и опытным разработчикам.
