Зачем фронтенд-инженерам изучать принципы компиляции?

JavaScript API внешний фреймворк
Зачем фронтенд-инженерам изучать принципы компиляции?
Эта статья была впервые опубликована вCSDN.NETЖурнал «Программист», декабрь 2017 г.

предисловие

Общее мнение состоит в том, что внешний интерфейс должен заложить три основы HTML, CSS и JS, глубоко понять семантические теги, понять N различных методов компоновки и освоить синтаксис, функции и встроенные API языка. Затем изучите некоторые основные интерфейсные фреймворки и используйте зрелую основу сообщества, чтобы быстро создать интерфейсный проект. Очень легко быть компетентным для фронтенд работы. Если вы продолжите изучение, вы обнаружите, что в области фронтенда всегда есть фреймворки, инструменты и библиотеки, которые невозможно изучить, и постоянно появляются новые колеса. Технология является инновационной, и версия быстро обновляется, но изменения остаются прежними. Инструмент предназначен для автоматизации и стандартизации процессов, обеспечивая лаконичное, элегантное и эффективное кодирование, а также решение задач с высоким уровнем абстракции и многоуровневости. В текущей ситуации, когда фронтенд-индустрия с открытым исходным кодом так горяча, пользователи фреймворка более тесно связаны с мейнтейнерами фреймворка, они могут не только углубиться в исходный код, чтобы лучше понять фреймворк, но и также задавайте вопросы, участвуйте в обсуждениях, добавляйте код и совместно решайте технические проблемы, чтобы способствовать развитию и росту экосистемы внешнего интерфейса. Принцип компиляции, как основная теоретическая дисциплина, помимо Помимо компилятора самого языка JS, он стал одним из теоретических краеугольных камней интерфейсных сред с открытым исходным кодом, таких как Babel, ESLint, Stylus, Flow, Pug, YAML, Vue, React и Marked. Понимание принципов компиляции может помочь вам лучше понять фреймворки, с которыми вы работаете.

Что такое компилятор?

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

Рисунок 1 Общий рабочий процесс исполняемой программы генерации исходной программы

Среди них компилятор делится на две части: front-end и back-end. Внешний интерфейс включает в себя лексический анализ, синтаксический анализ, семантический анализ и генерацию промежуточного кода и не зависит от машины.Более представительными инструментами являются Flex и Bison. Серверная часть включает промежуточную оптимизацию кода и генерацию объектного кода, которая зависит от машины.Более типичным инструментом является LLVM. В области интерфейсной веб-разработки из-за кроссплатформенных характеристик браузера хост-среды и Node.js нам нужно обратить внимание только на интерфейсную часть компилятора, чтобы в полной мере использовать его прикладную ценность. . Чтобы лучше понять принцип работы внешнего интерфейса компилятора, в этой статье в основном будет использоваться Babel, который широко используется, в качестве примера для объяснения того, как он компилирует исходный код в объектный код.

Babel

Являясь компилятором ES-грамматики нового поколения, Babel занимает очень важное место в цепочке интерфейсных инструментов: он строго следует языковой спецификации ECMA-262, чтобы анализировать последнюю грамматику, не дожидаясь обновлений браузера для обеспечения поддержки новых функций. В Babel используется парсер грамматики Babylon, а определение типа узла абстрактного синтаксического дерева (AST) относится к движку Mozilla JS SpiderMonkey, который расширен и улучшен и поддерживает синтаксический анализ грамматик Flow, JSX и TypeScript. Он использует Babylon для реализации двух частей компилятора: лексического анализа и синтаксического анализа.

лексический анализ

Лексический анализ — это первая часть обработки исходной программы, основная задача которой состоит в том, чтобы последовательно просмотреть входные символы, преобразовать их в последовательность лексических единиц (Token) и передать парсеру для разбора. Токен – это неделимая мельчайшая единица. Например, три символа var могут использоваться только целиком и не могут быть разделены семантически, поэтому это токен. Каждый объект Token имеет свойство типа и другие дополнительные свойства (приоритет оператора, номер строки/столбца и т. д.), которые можно идентифицировать по отдельности. В лексере Babylon каждое ключевое слово является Токеном, каждый идентификатор является Токеном, каждый оператор является Токеном, и каждый знак препинания также является Токеном. Токен. В дополнение к этому отфильтровываются комментарии и пробельные символы (переводы строк, пробелы, табуляции и т. д.) в исходной программе.

Правила сопоставления токенов могут быть описаны в соответствии с регулярными выражениями. Например, чтобы сопоставить Token типа Number, вы можете определить, начинается ли он с [0-9], а затем зациклить или рекурсивно сканировать следующие символы, и вам нужно обратить особое внимание на недесятичные значения, начинающиеся с 0b, 0o, 0x, научный Для специальных символов, таких как e или E, десятичная точка и т. д. в методе подсчета, указатель перемещается назад до тех пор, пока не будет выполнено правило сопоставления или не будет достигнут конец строки. Наконец, генерируется токен типа Number с такими атрибутами, как значение и расположение файла, и добавляется к последовательности токенов для продолжения следующего раунда сканирования.

Простой переход состояния числового типа показан на рис. 2:

Рис. 2. Схематическая диаграмма перехода состояния числового типа

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

Разбор

Синтаксический анализ является следующим шагом лексического анализа.Основная задача состоит в том, чтобы просмотреть последовательность Token, сгенерированную лексическим анализатором, построить AST в соответствии с определениями грамматики и типа узла и передать его остальной части интерфейса компилятора. Грамматика описывает правила построения языка программирования, которые определяют весь процесс синтаксического анализа. Он состоит из четырех частей: набора терминальных символов (также называемых жетонами), набора нетерминальных символов, набора продукций и начального символа. Например, производственное представление оператора объявления функции показано на рисунке 3:

Рис. 3. Создание оператора объявления функции

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

Синтаксические анализаторы делятся на нисходящий анализ и восходящий анализ в соответствии с их методами работы. Метод нисходящего анализа требует, чтобы AST был построен сверху (корневой узел) через крайнее левое производное.Обычно используемые анализаторы включают синтаксический анализатор рекурсивного спуска и синтаксический анализатор LL. Метод восходящего анализа требует построения AST снизу (конечный узел) через крайнее правое производное.Обычно используемые анализаторы включают синтаксический анализатор LR, синтаксический анализатор SLR и синтаксический анализатор LALR. Оба типа анализа практикуются в Вавилоне.

Первый — это нисходящий анализ, например операторы объявления переменных:

var foo = "bar";

После обработки лексером генерируется последовательность токенов:

Token('var')
Token('foo')
Token('=')
Token('"bar"')
Token(';')

Анализ рекурсивного спуска выполняется синтаксическим анализатором LL(1), просматривающим один входной токен за раз, чтобы решить, какое производственное расширение использовать. Для ПЕРВОГО набора оператора объявления переменной (первый набор токенов результата деривации) просто проверьте, что входной токен является одним из токенов ('var'), токенов ('let'), токенов ('const'), затем используйте это расширение производства. Сначала создайте VariableDeclaration, узел верхнего уровня AST, добавьте значение Token('var') к атрибуту узла, затем прочитайте оставшиеся токены один за другим и создайте их по очереди в соответствии с порядком нетерминальные символы производства слева направо. Дочерние узлы , продолжают рекурсивно спускаться до тех пор, пока все Токен считывается. Окончательный сгенерированный AST показан на рисунке 4:

Рис. 4. Дерево AST, созданное с помощью нисходящего анализа

Другой — восходящий анализ, например операторы выражений-членов:

foo.bar.baz.qux

Мы все знаем, что это утверждение эквивалентно:

((foo.bar).baz).qux

вместо:

foo.(bar.(baz.qux))

Причина в том, что разработанная им грамматика является леворекурсивной, а синтаксический анализатор LL не может анализировать леворекурсивную грамматику.В настоящее время только синтаксический анализатор LR может использоваться для построения AST снизу вверх. Основой синтаксического анализатора LR является метод анализа перемещения-уменьшения.Поддерживая стек, следующий входной токен решает, следует ли переместить его в стек или уменьшить некоторые символы в верхней части стека (заменить производственное тело на type header), сначала создайте дочерний узел, затем создайте родительский узел, пока все символы в стеке не будут уменьшены. Окончательный сгенерированный AST показан на рисунке 5:

Рис. 5. Дерево AST, созданное восходящим анализом

Кроме того, полный AST, построенный Babylon, также имеет специальные узлы верхнего уровня File и Program, которые описывают основную информацию о файле, типе модуля и так далее.

сгенерировать код

Компиляторы языков промышленного уровня обычно имеют этап семантического анализа, чтобы проверить, согласуется ли контекст программы с семантикой, определенной языком, например, проверка типа, проверка области, а другой — для создания промежуточного кода, такого как трехадресный код. , используя адрес и инструкции для линейного описания программы. Но поскольку позиционирование Babel — это всего лишь преобразование синтаксиса ES, эту часть работы можно передать движку интерпретатора JS. Наиболее отличительной частью Babel является его механизм подключаемых модулей, который вызывает разные подключаемые модули Babel для различных сред версии браузера. Через определение интерфейса шаблона посетителя (шаблон проектирования) выполняется обход AST в глубину, и указанные совпадающие узлы модифицируются, удаляются, добавляются и сдвигаются, так что исходный AST в другой модифицированный AST.

Интерфейс гостевого режима определяется следующим образом:

visitor: {
  Identifier(path) {
    enter() {
      //遍历AST进入Identifier结点时执行
      ... 
    },
    exit() {
      //遍历AST离开Identifier结点时执行
      ...
    }
  },
  ...
}

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

шаблонизатор

Говоря о механизмах шаблонов, он впервые появился при разработке динамических страниц на стороне сервера, таких как JSP, PHP, ASP и др. С момента быстрого развития Node.js индустрия клиентских интерфейсов произвела множество колеса, в том числе EJS, Handlebars, Pug (ранее Jade), Mustache, и этот список можно продолжить. Технология механизма шаблонов делает более гибким отображение представлений в сочетании с данными и предоставляет больше возможностей для абстракции логики, а данные и содержимое не зависят друг от друга. Существует множество способов реализации механизмов шаблонов. Простые механизмы шаблонов могут быть реализованы напрямую путем замены и объединения строк. Более сложные механизмы шаблонов, такие как Pug, будут иметь относительно полный процесс лексического и синтаксического анализа. JS-код выполняется динамически.

Например шаблон заявления:

h1 hello #{name}

AST, сгенерированный парсером Pug, показан на рисунке 6:

Рис. 6. AST, сгенерированный парсером Pug

Код объекта, генерируемый генератором (псевдокод):

'<h1>' + 'hello' + name + '<h1>'

Во время выполнения вызовите новую функцию для динамического выполнения кода:

var compiledFn = new Function('local', `
  with (local) {
    return '<h1>' + 'hello' + name + '<h1>'; 
  }
`)

compiledFn({
  name: 'world'
})

Окончательный выходной HTML-оператор:

<h1>hello world</h1>

Весь процесс состоит из двух частей: фазы прекомпиляции и фазы выполнения. Конечно, хороший механизм шаблонов также будет учитывать как функциональность, производительность и безопасность.Следует избегать приведенного выше оператора `with`, а также механизма кэширования, механизма предотвращения XSS и более мощного, дружественного и простого в использовании синтаксического сахара. также следует ввести.

Также стоит упомянуть, что интерфейсные фреймворки MVVM, представленные Angular, React и Vue, используют технологию компиляции шаблонов. Многие разработчики предпочитают Vue как прогрессивное внешнее решение, поскольку оно предоставляет функции рендеринга и шаблоны для рендеринга представлений. Использование функции рендеринга требует вызова основного API для создания типа Virtual DOM. Этот процесс относительно сложен, а объем кода очень велик. Как только уровень DOM становится глубоко вложенным, код становится трудно контролировать и поддерживать. . Чтобы справиться с этой сложностью, другим способом является написание шаблонов на основе HTML и добавление уникальных тегов, директив, интерполяции и другого синтаксиса Vue, и пусть компилятор компилирует и оптимизирует от шаблонов до функций рендеринга, что более эффективно, чем Элегантный, удобный и простой в кодировании.

Препроцессор CSS

Метод внешнего интерфейса развился из чистой эры слэша и записи CSS в языки предварительной обработки, представленные Sass, Less и Stylus, дающие CSS возможность программирования, определяющие переменные, функции, вычисления выражений, модульность и другие. функции, что значительно повышает производительность разработчика. Это изменения, вызванные технологией компиляции. Аналогичным образом компилятор лексически анализирует исходный код, создавая последовательность токенов. Затем синтаксический анализ генерирует промежуточное представление, AST, которое соответствует определению. В то же время для каждого блока программы будет создана таблица символов для записи имен и атрибутов переменных, что поможет анализу объема переменных на этапе генерации кода. Наконец, рекурсивно спуститесь по AST, чтобы сгенерировать исполняемый файл, который можно запустить непосредственно в среде браузера. CSS-код.

Возьмем в качестве примера синтаксис препроцессора Stylus:

foo = 14px

body
  font-size foo

Скомпилированный AST показан на рисунке 7:

Рис. 7. AST, сгенерированный парсером Stylus

Окончательный сгенерированный объектный код:

body {
  font-size: 14px;
}

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

напиши в конце

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