В этой статье мы собираемся немного углубиться в среду Node и понять, как она работает под поверхностью. Темы, которые будут охвачены:

  1. Как среда выполнения и движок JS связаны друг с другом?
  2. Каковы некоторые обязанности V8?
  3. Стек вызовов и куча
  4. Основные модули и привязки C ++
  5. Потоки и асинхронный код в NodeJs
  6. Цикл событий
  7. Очередь событий (скоро…)
  8. "Вывод"

Как среда выполнения и движок JS связаны друг с другом?

Nodejs - это среда выполнения для запуска Javascript. Вы могли бы сказать, что такое среда выполнения? Возьмем, к примеру, браузер.

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

Среда выполнения - это место в вашей системе, где вы можете использовать общие библиотеки, переменные среды и фактически выполнять свой код. Например, в браузере вы можете получить доступ к глобальным переменным документа и окна и работать с ними. В Node также есть общие переменные, которые вы можете использовать в его среде выполнения, о чем мы поговорим позже.

Safari, Firefox, Chrome имеют свои собственные среды выполнения, в этих средах есть свои собственные механизмы Javascript, которые соответственно называются Chakra, SpiderMonkey и V8. Именно о последнем мы сейчас заботимся.

Каковы некоторые из обязанностей V8?

Помимо преобразования кода Javascript в машинный код, Engine также выполняет некоторые другие обязанности:

  1. Он предоставляет сборщик мусора → Используется для очистки хранилища переменных и функций, которые больше не используются при выполнении (вне области действия)
  2. Выполняет ваш код и преобразует Javascript в машинный код, чтобы компьютер мог понять, как упоминалось ранее
  3. Управление выделением памяти
  4. Предоставьте все типы данных, операторы, объекты и функции
  5. Обработка стека вызовов (выполнение кода по порядку)
  6. Цикл событий

Подробнее о том, что V8 делает на этой линке k

Стек вызовов и куча

Стек вызовов - это та часть NodeJs, которая отслеживает выполнение вашего приложения. По сути, это FILO (First in Last Out), который точно знает, где вы находитесь в процессе исполнения.

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

Вот простой пример:

function multiply(n, n){
    return n * n
}
function square(n){
    const resultMultiply = multiply(n, n)
    return resultMultiply
}
function squaredNumber(n){
    const result = square(n)
    return result
}

Приведенный выше код заполнит наши вызовы стек и кучу.

Ниже вы видите, что происходит в действии:

Базовые модули и привязки C ++

Этот образ используется повсюду, но многим людям сложно понять. На самом деле это довольно просто. Попробуйте посмотреть на него горизонтально.

  • Наверху у вас есть код Javacript.
  • В средней части находится движок V8 вместе с основными модулями, привязками C ++, libuv, http-запросами и т. Д.
  • А внизу у нас есть операционная система.

Средняя часть - это то, что для нас сейчас наиболее важно.

Основные модули

Эти модули состоят из некоторых объектов для расширения возможностей Javascript на стороне сервера. Некоторые из наиболее часто используемых и некоторые примеры приведены ниже:

  • http → Используется для выполнения HTTP-запросов.
http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' })
    res.end('This is a test for creating a Nodejs server')
}).listen(3000)
  • fs → Используется для работы с файловой системой (создание файлов, удаление, потоковая передача и т. д.).
const createFile = (filePath, fileContent) => {
    fs.writeFile(filePath, fileContent, (error) => {
    if(error)
        console.error('an error occurred: ', error)
    else
        console.info('Your file is made')
})
  • path → Используется для работы с путями к файлам
path.join(DIRNAME, 'media')

И так далее ... Есть много функций и объектов, которые появляются прямо из коробки с Nodejs. Если хотите, вы можете изучить их, загрузив Node на свой компьютер и запустив node, а затем дважды щелкнув tab.

> node
Welcome to Node.js v13.13.0.
Type ".help" for more information.
> 
Array                 ArrayBuffer           Atomics
BigInt                BigInt64Array         BigUint64Array
Boolean               Buffer                DataView
Date                  Error                 EvalError
Float32Array          Float64Array          Function
GLOBAL                Infinity              Int16Array
Int32Array            Int8Array             Intl
JSON                  Map                   Math
NaN                   Number                Object
Promise               Proxy                 RangeError
ReferenceError        Reflect               RegExp
Set                   SharedArrayBuffer     String
Symbol                SyntaxError           TextDecoder
TextEncoder           TypeError             URIError
URL                   URLSearchParams       Uint16Array
Uint32Array           Uint8Array            Uint8ClampedArray
WeakMap               WeakSet               WebAssembly
_                     _error                assert
async_hooks           buffer                child_process
clearImmediate        clearInterval         clearTimeout
cluster               console               crypto
decodeURI             decodeURIComponent    dgram
dns                   domain                encodeURI
encodeURIComponent    escape                eval
events                fs                    global
globalThis            http                  http2
https                 inspector             isFinite
isNaN                 module                net
os                    parseFloat            parseInt
path                  perf_hooks            process
punycode              querystring           queueMicrotask
readline              repl                  require
root                  setImmediate          setInterval
setTimeout            stream                string_decoder
tls                   trace_events          tty
undefined              unescape              url
util                  v8                    vm
worker_threads        zlib

Привязки C ++

Привязки C ++ относятся к способам использования кода C ++, если вы хотите использовать надстройки в своем проекте. Поскольку Nodejs написан на C ++, вы можете легко связать эти скрипты для работы бок о бок с вашим проектом Nodejs.

Но зачем вам это использовать?

  1. Вы можете сделать это по соображениям производительности.
  2. Возможно, вы захотите интегрировать стороннюю библиотеку, написанную на C ++, в свой проект Nodejs.
  3. Или, возможно, получить доступ к некоторым API-интерфейсам ОС, которые сложно реализовать с использованием только Nodejs.

Потоки и асинхронный код в NodeJS

Действительно ли NodeJs однопоточный?

Вы когда-нибудь слышали, что Nodejs однопоточный? Он однопоточный, но это не всегда так. Что это на самом деле означает?

Подробнее о потоках…

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

вы думаете о полосе шоссе (процесс). Чем больше полос (потоков) на этой автомагистрали, тем больше автомобилей (данных) сможет добраться до места назначения (будет выполнено)

«Итак… если Nodejs такой быстрый и надежный и однопоточный, как он может справиться с таким количеством данных, отправляемых и получаемых одновременно?»

Асинхронный код в Javascript

Асинхронный характер Nodejs - вот что делает заслугу всего этого повышения производительности. Чтобы упростить асинхронность, я думаю, что лучший способ объяснить это:

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

Для тех, кому было трудно понять, есть более простая аналогия:

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

Теперь, если официант ждал, пока ваш заказ будет готов ждать кого-то еще, это будет синхронное действие.

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

Для этого Nodejs предоставляет асинхронные функции обратного вызова или обещания. Вот пример использования обещаний:

const getUserInfo = (userId) => new Promise((resolve, reject) => {
   const user = getUserOnDatabase(userId).then(res => {
       if(res)
         resolve(res)
       else
         reject()
})
getUserInfo(userId)
    .then((res) => {
        //continues the execution here
    })
    .catch(err => {})
...

Итак, что здесь происходит?

getUserInfo() получает некоторую информацию о пользователе. Однако, поскольку мы создали обещание для обработки этого запроса, для получения информации потребуется некоторое время. Мы не могли заблокировать выполнение кода, поэтому сразу после выполнения getUserInfo() остальная часть кода (представленная ...) будет выполнена, а то, что обернуто вокруг нашего thenmethod, будет ждать разрешения для выполнения обещания.

Так что NodeJs однопоточный или многопоточный?

Вы думали, я забыл об этом, не так ли? Но на самом деле это не вопрос «да» или «нет», это скорее вопрос зависимости.

Для некоторых асинхронных вызовов библиотека libuv предоставляет пул потоков с четырьмя потоками для обработки кода. Это означает, что программа продолжает выполняться в одном потоке - назовем его «основным потоком» - и делегирует отдельную часть с еще четырьмя потоками для асинхронных вызовов… Итак, в этом случае у вас есть больше потоков, идущих рядом друг с другом.

Однако в настоящее время все больше и больше ОС предоставляют асинхронные интерфейсы для многих задач ввода-вывода, и libuv предпочитает этот вариант, чем использование пула потоков. В этом случае преобладает основной поток.

Некоторые другие обязанности libuv:

  1. Сокеты TCP и UDP пакета net
  2. Асинхронные разрешения DNS
  3. Асинхронные операции с файлами и файловой системой
  4. События файловой системы

и так далее ... подробнее о libuv здесь

Libuv также можно назвать уровнем, который абстрагирует API обработки ввода-вывода от верхних уровней Nodejs. Наряду с этим libuv также предоставляет цикл событий для всей среды выполнения NodeJS.

Цикл событий? Как это работает ?

Каждый раз, когда вы запускаете свой процесс узла (как было сказано выше), выполняется цикл событий. Таким образом, в одном потоке одновременно работают две разные части: V8 и цикл событий.

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

Учитывая изображение выше, вы можете спросить: что за $ @! такое демультиплексор событий? А как насчет низкоуровневых механизмов ввода-вывода?

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

В основном происходит следующее:

  1. вы запускаете процесс Nodejs, выполняя что-то вроде node example.js… Затем ваше приложение запускает цикл событий.
  2. Всякий раз, когда есть асинхронный обратный вызов, обещание или какой-либо асинхронный API, Node будет вызывать API libuv для обработки этих функций.
  3. Эти API-интерфейсы будут взаимодействовать с низкоуровневыми механизмами ввода-вывода.
  4. Эти механизмы будут отправлять обратно обработанные функции в libuv (или демультиплексор событий).
  5. Затем Libuv отправляет обработанные данные (данные из файла, некоторый запрос, на который был получен ответ… и т. Д.) В свою знаменитую очередь событий, о чем я немного расскажу.
  6. После получения данных из очереди они будут отправлены в стек вызовов для выполнения после выполнения всего синхронного кода.

И затем весь этот цикл повторяется снова и снова, пока процесс не завершится и код не перестанет выполняться.

Очередь событий

вскоре…

Вывод

Javascript - это постоянное обучение. Это очень мощный инструмент, поскольку он создан на основе очень надежного и быстрого языка (c ++) и может использоваться для нескольких типов приложений благодаря циклу событий и асинхронности языка. Однако крайне важно, чтобы вы понимали, как это работает под поверхностью, чтобы извлечь максимальную пользу из этого мощного инструмента.

Обратите внимание, что я буду обновлять статью и скоро расскажу больше о очереди событий.

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

Хорошего дня!