JavaScript под капотом

Nov 28 2022
Содержание В этой статье мы погрузимся во внутреннюю работу JavaScript и то, как он работает на самом деле. Разобравшись в деталях, вы поймете поведение своего кода и, следовательно, сможете писать более качественные приложения.

Оглавление

  • Поток и стек вызовов
  • Контекст выполнения
  • Цикл событий и асинхронный JavaScript
  • Хранение памяти и сборка мусора
  • JIT (Just In Time) компиляция
  • Резюме

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

JavaScript описывается как:

Однопоточный язык программирования со сборкой мусора, интерпретируемый или компилируемый Just In Time язык программирования с неблокирующим циклом обработки событий.

Давайте раскроем каждый из этих ключевых терминов.

Стек потоков и вызовов:

Механизм JavaScript — это однопоточный интерпретатор, состоящий из кучи и одного стека вызовов, который используется для выполнения программы.
Стек вызовов — это структура данных, в которой используется принцип «последним пришел — первым обслужен» (LIFO) для временного хранения и управления вызовом функции (вызовом).
Это означает, что последняя функция, которая помещается в стек, будет первой, которая будет извлечена из нее при возвращении из функции.
Поскольку стек вызовов один, выполнение функций выполняется по одному, сверху вниз. Это означает, что стек вызовов является синхронным.

Теперь, поскольку он синхронный, вам будет интересно, как JavaScript может обрабатывать асинхронные вызовы?
Цикл событий — это секрет асинхронного программирования JavaScript.
Но прежде чем объяснять концепцию асинхронных вызовов в JavaScript и то, как это возможно с однопоточным языком, давайте сначала разберемся, как выполняется код.

Контекст выполнения (EC):

Контекст выполнения определяется как среда, в которой выполняется код JavaScript.
Создание контекста выполнения происходит в два этапа:

1. Фаза создания памяти:

  • Создание глобального объекта (который называется объектом окна в браузере и глобальным объектом в NodeJS).
  • Создание объекта this и привязка его к глобальному объекту.
  • Настройка кучи памяти (куча — это большая, в основном неструктурированная область памяти) для хранения ссылок на переменные и функции.
  • Хранение функций и переменных в глобальном контексте выполнения путем реализации Hoisting .

Теперь, когда мы знаем этапы выполнения кода, давайте вернемся к

Цикл событий:

Во-первых, давайте начнем с рассмотрения этой диаграммы:

Цикл событий в JS

У нас есть движок, который состоит из двух основных компонентов:
* Куча памяти — здесь происходит выделение памяти.
* Стек вызовов — здесь находятся кадры стека во время выполнения кода.

У нас есть веб-API, представляющие собой потоки, к которым вы не можете получить доступ, вы можете просто вызывать их. Это части браузера, в которых включается параллелизм, например DOM, AJAX, setTimeout и многие другие.

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

Итак, какова задача цикла событий здесь?
Цикл событий выполняет одну простую задачу — отслеживать стек вызовов и очередь обратного вызова. Если стек вызовов пуст, цикл событий возьмет первое событие из очереди и поместит его в стек вызовов, который эффективно его запустит.
Такая итерация называется тиком в цикле событий. Каждое событие — это просто обратный вызов функции.

Хранение памяти и сборка мусора:

Чтобы понять необходимость сборки мусора, мы должны сначала понять жизненный цикл памяти, который практически одинаков для любого языка программирования и состоит из 3 основных этапов.
1. Выделить память.
2. Используйте выделенную память либо для чтения, либо для записи, либо для того и другого.
3. Освободить выделенную память, когда она больше не требуется.

Большинство проблем с управлением памятью возникает, когда мы пытаемся освободить выделенную память. Основная проблема, которая возникает, это определение неиспользуемых ресурсов памяти.
В случае языков низкого уровня, где разработчик должен вручную решить, когда память больше не нужна, языки высокого уровня, такие как JavaScript, используют автоматизированную форму управления памятью, известную как сборка мусора (GC).
JavaScript использует две известные стратегии для выполнения GC: метод подсчета ссылок и алгоритм маркировки и очистки.
Вот подробное объяснение от MDN по обоим алгоритмам и тому, как они работают.

JIT (Just In Time) Компиляция:

Вернемся к определению JavaScript: там написано «интерпретируемый, JIT-компилируемый язык программирования», так что же это вообще означает? Как насчет того, чтобы начать с разницы между компилятором и интерпретатором в целом?

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

Таким образом, скомпилированные языки имеют медленное время записи и быстрое время выполнения, а интерпретируемые языки имеют противоположное.

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

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

А вот и роль компилятора JIT, которая улучшает производительность интерпретируемых программ. Весь код сразу преобразуется в машинный код, а затем немедленно выполняется .

Внутри компилятора JIT у нас есть новый компонент, называемый монитором (он же профилировщик). Этот монитор наблюдает за выполнением кода и

  • Определите горячие или теплые компоненты кода, например: повторяющийся код.
  • Преобразуйте эти компоненты в машинный код во время выполнения.
  • Оптимизируйте сгенерированный машинный код.
  • Горячая замена предыдущей реализации кода.

Теперь, когда мы поняли основные концепции, давайте на минутку соберем все воедино и суммируем шаги, которые JS Engine выполняет при выполнении кода:

Источник изображения: traversy media channel
  1. JS Engine берет код JS, написанный с удобочитаемым синтаксисом, и превращает его в машинный код.
  2. Движок использует синтаксический анализатор, чтобы просмотреть код построчно и проверить правильность синтаксиса. Если есть какие-либо ошибки, код перестанет выполняться и будет выброшена ошибка.
  3. Если все проверки проходят успешно, синтаксический анализатор создает древовидную структуру данных, называемую абстрактным синтаксическим деревом (AST).
  4. AST — это структура данных, представляющая код в виде древовидной структуры. Легче превратить код в машинный код из AST.
  5. Затем интерпретатор берет AST и превращает его в IR, который является абстракцией машинного кода и посредником между JS-кодом и машинным кодом. IR также позволяет проводить оптимизации и является более мобильным.
  6. Затем JIT-компилятор берет сгенерированный IR и превращает его в машинный код, компилируя код, получая обратную связь на лету и используя эту обратную связь для улучшения процесса компиляции.

Спасибо за чтение :)

Вы можете подписаться на меня в Twitter и LinkedIn .