Краткая иллюстрация процесса обработки скриптов движком JS
Данный текст является переводом оригинальной статьи 🚀⚙️ JavaScript Visualized: the JavaScript Engine авторства Lydia Hallie. В процессе адаптации на русский язык я попыталась сохранить стиль автора.
Статья является частью серии из 7-ми текстов, в которой Лидия доступно, с живыми иллюстрациями и юмором, раскрывает одни из самых базовых и, вместе с тем, сложных для понимания концепций и принципов JavaScript.
Вступление
JavaScript очень крут (и не только на мой взгляд). Но как, спросите Вы, машина может действительно понимать код, который мы пишем? Нам, JavaScript разработчикам, обычно не приходится иметь дело непосредственно с компиляторами. Однако, неплохо бы иметь базовые представления об устройстве ядра JavaScript и знать, как в нём наш human-friendly код обрабатывается и преобразуется в машинные команды. 🥳
Внимание! Данный пост, в основном, опирается на движок V8, используемый в Node.js и Chromium-подобных браузерах.
Итак. Что же происходит “за кулисами”?
Этап 1. Поток байтов
Парсер HTML сталкивается с тэгом script
с указанием источника. Код из этого источника запрашивается из сети, кэша или установленного сервис воркера (service worker). Ответом на запрос является скрипт, представленный в виде потока байтов. Забота об этом потоке поручается специально обученному декодеру потока байтов, который обрабатывает данные по мере загрузки :)
1 || Скрипт загружается в виде потока байтов в кодировке UTF-16 из сети, кэша или воркера и передаётся в декодер потока байтов
Этап 2. Декодер потока байтов
Декодер потока байтов создаёт специальные токены на основе поступившего потока. Так, например, 0066
преобразуется в f
, 0075
— в u
, 006e
— в n
, 0063
— в c
, 0074
— в t
, 0069
— в i
, 006f
— вo
, а 006e
превратится в n
с последующим пробелом. Кажется, здесь было закодировано ключевое слово function
! В JavaScript это одно из специальных зарезервированных слов. Итак токен был создан и отправлен в парсер (и препарсер, что пока не отображено в иллюстрациях, потому что мы вернёмся к этому моменту позже). Похожая судьба постигает и все остальные символы во входящем потоке байтов.
2 || Декодер потока байтов переводит байты в токены. Токены отправляются в парсер
Этап 3. Парсер и Абстрактное Синтаксическое Дерево
Ядро движка на самом деле состоит аж из двух парсеров: препарсера (pre-parser) и парсера (parser). В попытках сократить время загрузки веб-сайта, движок старается не парсить код, в котором нет необходимости сразу, прямо сейчас. Препарсер обрабатывает код, который может быть использован позже, в то время как парсер работает над кодом, который нужен “здесь и сейчас”!
Например, если какая-то функция вызывается только когда пользователь нажимает на кнопку, нет нужды компилировать эту часть кода немедленно для загрузки сайта. А вот, когда пользователь вызовет событие клика по кнопке, запрашивая тем самым конкретную функцию, тогда парсер обработает её.
Парсер генерирует узлы (ноды), основанные на токенах, полученных из декодера потока байтов. Из этих узлов формируется Абстрактное Синтаксическое Дерево (Abstract Syntax Tree, AST). 🌳
3 || Парсер генерирует узлы, основанные на токенах, и создаёт Абстрактное Синтаксическое Дерево
Этап 4. Интерпретатор
Наконец, в дело вступает интерпретатор! Интерпретатор, который проходит через AST, и генерирует байтовый код, основанный на информации, полученной от AST. После того, как код сгенерирован полностью, Абстрактное Синтаксическое Дерево удаляется, очищая память. Итак, наконец, мы имеем что-то, с чем может работать наша машина (компьютер/телефон и т.д.)! 🎉
4 || Интерпретатор проходится по AST (Абстрактное Синтаксическое Дерево) и генерирует байтовый код
Этап 5. Оптимизирующий компилятор
Хотя байт-код быстрый, он может быть быстрее. По мере выполнения этого байт-кода генерируется информация. Он может определить, часто ли повторяемся определенное поведение, а также типы используемых данных. Возможно, вы вызываете какую-то функцию десятки раз: пришло время ее оптимизировать, чтобы она работала еще быстрее! 🏃🏽♀️
Байт-код вместе с фидбеком сгенерированного типа отправляется оптимизирующему компилятору. Оптимизирующий компилятор принимает байт-код и фидбэк типа и генерирует на их основе высокооптимизированный машинный код. 🚀
5 || Байт–код и фидбек типа отправляются в оптимизирующий компилятор, который генерирует высоко оптимизированный машинный код
Про работу движка с динамической типизацией
JavaScript — это динамически типизированный язык, а это означает, что типы данных могут постоянно меняться. Было бы очень медленно, если бы движку JavaScript приходилось каждый раз проверять, какой тип данных имеет определенное значение.
Чтобы сократить время, необходимое для интерпретации кода, оптимизированный машинный код обрабатывает только те случаи, которые движок видел раньше при запуске байт-кода. Если мы неоднократно использовали определенный фрагмент кода, который снова и снова возвращал один и тот же тип данных, оптимизированный машинный код можно просто использовать повторно, чтобы ускорить процесс. Однако, поскольку JavaScript является динамически типизированным, может случиться так, что один и тот же фрагмент кода внезапно вернет данные другого типа. Если это произойдет, машинный код будет деоптимизирован, и движок вернется к интерпретации сгенерированного байт-кода.
Допустим, определенная функция вызывается 100 раз и до сих пор всегда возвращала одно и то же значение. Предполагается , что он также вернет это значение при 101-м вызове.
Допустим, у нас есть некоторая функция sum
, которая (до сих пор) всегда вызывалась с числовыми значениями в качестве аргументов каждый раз:
function sum(a, b) {
return a + b
}
sum(1, 2)
Вызов функции в примере выше вернёт число 3
! В следующий раз, когда мы вызовем нашу функцию, движок будет считать, что мы вызываем её снова с двумя числовыми значениями.
Если это правда, динамический поиск не требуется, и можно просто повторно использовать оптимизированный машинный код. В противном случае, если предположение было неверным, вместо оптимизированного машинного кода произойдет возврат к исходному байтовому коду.
Например, при следующем вызове мы передадим строку вместо числа. Поскольку JavaScript является динамически типизированным, мы можем сделать это без каких-либо ошибок!
function sum(a, b) {
return a + b
}
sum('1', 2)
В таком случае, число 2
будет преобразовано в строку, и вместо числа 3 функция вернет строку "12"
. Движок JavaScript возвращается к выполнению интерпретированного байт-кода и обновляет обратную связь о типе.
Заключение
Надеюсь, этот пост был полезен для вас! Конечно, остаётся ещё много частей ядра JS, которые я не раскрыла в этом материале (JS heap, стек вызовов, и т.д.), о которых я расскажу позже! Я определенно рекомендую вам начать самостоятельно проводить исследования, если вы интересуетесь внутренними особенностями JavaScript. V8 имеет открытый исходный код и имеет отличную документацию о том, как он работает под капотом! 🤖
Полезные ссылки: V8 Docs || V8 Github || Chrome University 2018: Life Of A Script
Ролик на YouTube на английском, где Лидия ещё более наглядно разбирает устройство движка JavaScript:
Understanding the V8 JavaScript Engine
Lydia Hallie в соцсетях: Twitter || Instagram || GitHub || LinkedIn || Website
P.S. Автор использовала Keynote для создания анимаций и записей экрана.
P.P.S. В следующих статьях будут рассмотрены такие штуки, как Поднятие (hoisting), Цикл событий (event loop), область действия (scope chain), прототипное наследование (prototypal inheritance), генераторы и итераторы (generators ans iterators), промисы (promises) и async/await
Top comments (0)