прошлое
- Серия интервьюеров (1): Как реализовать глубокое клонирование
- Серия интервьюеров (2): Реализация шины событий
- Серия интервьюеров (3): Реализация внешней маршрутизации
- Серия интервьюеров (4): Преимущества двустороннего связывания на основе перехвата данных прокси
- Серия интервью (5): Почему вы используете интерфейсный фреймворк
- Interviewer Series (6): Вы когда-нибудь писали «Универсальные интерфейсные компоненты»?
предисловие
Babel — это современный преобразователь синтаксиса JavaScript. Его можно увидеть практически в любом современном интерфейсном проекте. Принцип, лежащий в его основе, до сих пор остается черным ящиком для большинства разработчиков, но Babel действительно необходим как инструмент для понимания принципа, лежащего в его основе.
Если это просто Babel, то может и не понадобиться, проблема в том, что принцип, лежащий в его основе, слишком широко используется в нашей разработке, включая, но не ограничиваясь: eslint jshint stylelint css-in-js prettier jsx vue-template uglify-js postcss less и так далее, от шаблонов до обнаружения кода, от сжатия обфускации до преобразования кода и даже подсветки кода редактора тесно связаны.
Если вам интересно, вы можете заняться черной магией:Что могут сделать фронтенд-инженеры с принципами компиляции?
Каталог статей
- Разбор кода (реализация парсера)
- Преобразование кода (реализующий трансформатор)
- генерация кода
передний
Вавилон условно делится на три части:
- Разбор: преобразование кода (фактически строки) в AST (абстрактное синтаксическое дерево).
- Преобразование: посетите узлы AST, чтобы выполнить операции преобразования для создания нового AST.
- Генерировать: генерировать код на основе нового AST.
Мы в основном понимаем основные принципы Babel, создавая миниатюрный Babel.Функция этого миниатюрного Babel очень проста и безвкусна, но все еще есть 400 строк кода, и его детали реализации не такие, как у Babel, потому что мы экономим много дополнительной проверки и анализа информации, потому что один парсер, совместимый с современным синтаксисом JavaScript, требует 5000 строк кода, что не способствует нашему быстрому пониманию базовой реализации Babel, поэтому этот миниатюрный Babel можно назвать довольно безвкусным. (потому что это бесполезно, кроме как для отображения), но относительно полно показывает основные принципы Babel. Вы можете использовать это как введение. Если вы все еще заинтересованы после введения, вы можете прочитать:
- спецификация эстри
- acorn: Легкий современный парсер JavaScript, Babel изначально был основан на этом проекте.
1. Анализ кода
1.1 Концепция парсера
Анализ кода, также известный как Parser, используется для анализа фрагмента кода (текста) в структуру данных.
Например, этот код es6
const add = (a, b) => a + b
Это форма, которую мы анализируем с помощью babel:
{
"type": "File",
"start": 0,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 27
}
},
"program": {
"type": "Program",
"start": 0,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 27
}
},
"sourceType": "module",
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 27
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 6
},
"end": {
"line": 1,
"column": 27
}
},
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"loc": {
"start": {
"line": 1,
"column": 6
},
"end": {
"line": 1,
"column": 9
},
"identifierName": "add"
},
"name": "add"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 12,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 12
},
"end": {
"line": 1,
"column": 27
}
},
"id": null,
"generator": false,
"expression": true,
"async": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"loc": {
"start": {
"line": 1,
"column": 13
},
"end": {
"line": 1,
"column": 14
},
"identifierName": "a"
},
"name": "a"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 16
},
"end": {
"line": 1,
"column": 17
},
"identifierName": "b"
},
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"start": 22,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 22
},
"end": {
"line": 1,
"column": 27
}
},
"left": {
"type": "Identifier",
"start": 22,
"end": 23,
"loc": {
"start": {
"line": 1,
"column": 22
},
"end": {
"line": 1,
"column": 23
},
"identifierName": "a"
},
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 26,
"end": 27,
"loc": {
"start": {
"line": 1,
"column": 26
},
"end": {
"line": 1,
"column": 27
},
"identifierName": "b"
},
"name": "b"
}
}
}
}
],
"kind": "const"
}
],
"directives": []
}
}
Давайте напишем простой синтаксический анализатор с целью анализа стрелочной функции es6 выше.
В процессе создания текста ---> AST есть два ключевых этапа:
- Лексический анализ: разбить код (строку) на поток токенов, т.е.синтаксическая единицамассив
- Разбор: проанализируйте поток токенов (массив, сгенерированный выше) и сгенерируйте AST.
1.2 Лексический анализ (Tokenizer -- Lexical Analyzer)
Чтобы провести лексический анализ, сначала нам нужно понять, что такое JavaScript.синтаксическая единица
- Числа: научная нотация в JavaScript, как и обычные массивы, являются синтаксическими единицами.
- Скобки: "(" ")" до тех пор, пока они появляются, это грамматическая единица, независимо от ее значения.
- Идентификатор: непрерывные символы, общие переменные, константы (например: null true), ключевые слова (если разрыв) и т. д.
- Операторы: +, -, *, / и т.д.
- Конечно, есть комментарии, скобки и т.д.
В процессе нашего парсера мы должны посмотреть на код под другим углом, код, который мы обычно используем, это по сути строка или кусок текста, он не имеет смысла, это движок JavaScript придает ему смысл, поэтому мы кодируем в процессе синтаксического анализа Просто кусок строки.
Все еще возьмите следующий код в качестве примера
const add = (a, b) => a + b
Мы ожидаем, что результат будет примерно таким
[
{ type: "identifier", value: "const" },
{ type: "whitespace", value: " " },
...
]
Итак, теперь мы начинаем создавать Tokenizer (лексический анализатор)
// 词法分析器,接收字符串返回token数组
export const tokenizer = (code) => {
// 储存 token 的数组
const tokens = [];
// 指针
let current = 0;
while (current < code.length) {
// 获取指针指向的字符
const char = code[current];
// 我们先处理单字符的语法单元 类似于`;` `(` `)`等等这种
if (char === '(' || char === ')') {
tokens.push({
type: 'parens',
value: char,
});
current ++;
continue;
}
// 我们接着处理标识符,标识符一般为以字母、_、$开头的连续字符
if (/[a-zA-Z\$\_]/.test(char)) {
let value = '';
value += char;
current ++;
// 如果是连续字那么将其拼接在一起,随后指针后移
while (/[a-zA-Z0-9\$\_]/.test(code[current]) && current < code.length) {
value += code[current];
current ++;
}
tokens.push({
type: 'identifier',
value,
});
continue;
}
// 处理空白字符
if (/\s/.test(char)) {
let value = '';
value += char;
current ++;
//道理同上
while (/\s]/.test(code[current]) && current < code.length) {
value += code[current];
current ++;
}
tokens.push({
type: 'whitespace',
value,
});
continue;
}
// 处理逗号分隔符
if (/,/.test(char)) {
tokens.push({
type: ',',
value: ',',
});
current ++;
continue;
}
// 处理运算符
if (/=|\+|>/.test(char)) {
let value = '';
value += char;
current ++;
while (/=|\+|>/.test(code[current])) {
value += code[current];
current ++;
}
// 当 = 后面有 > 时为箭头函数而非运算符
if (value === '=>') {
tokens.push({
type: 'ArrowFunctionExpression',
value,
});
continue;
}
tokens.push({
type: 'operator',
value,
});
continue;
}
// 如果碰到我们词法分析器以外的字符,则报错
throw new TypeError('I dont know what this character is: ' + char);
}
return tokens;
};
Затем создается наш базовый лексер, потому что он нацелен только на эту функцию es6, поэтому никакой дополнительной работы не выполняется (дополнительная работа будет огромной).
const result = tokenizer('const add = (a, b) => a + b')
console.log(result);
/**
[ { type: 'identifier', value: 'const' },
{ type: 'whitespace', value: ' ' },
{ type: 'identifier', value: 'add' },
{ type: 'whitespace', value: ' ' },
{ type: 'operator', value: '=' },
{ type: 'whitespace', value: ' ' },
{ type: 'parens', value: '(' },
{ type: 'identifier', value: 'a' },
{ type: ',', value: ',' },
{ type: 'whitespace', value: ' ' },
{ type: 'identifier', value: 'b' },
{ type: 'parens', value: ')' },
{ type: 'whitespace', value: ' ' },
{ type: 'ArrowFunctionExpression', value: '=>' },
{ type: 'whitespace', value: ' ' },
{ type: 'identifier', value: 'a' },
{ type: 'whitespace', value: ' ' },
{ type: 'operator', value: '+' },
{ type: 'whitespace', value: ' ' },
{ type: 'identifier', value: 'b' } ]
**/
1.3 Синтаксический анализ
Синтаксический анализ намного сложнее, чем лексический, потому что дальше мы собираемсякод схемы, поэтому делается много «произвольных» суждений о пропуске кода, и даже это самый большой объем кода во всем микро-бабеле.
Причина сложности синтаксического анализа заключается в том, что для анализа возможности различных синтаксисов разработчикам необходимо проанализировать логическую взаимосвязь между кодами в соответствии с информацией, предоставленной потоком токенов (массив токенов, который мы сгенерировали в предыдущем разделе), и только после этого. лексический анализ Поток токенов может стать структурированным абстрактным синтаксическим деревом.
Парсинг лучше всего делать по стандарту, большинство парсеров JavaScript следуютспецификация эстри
Так как существует много стандартов, вы можете ознакомиться с ними, если вам это интересно.В настоящее время мы представляем только еще несколько важных стандартов:
Операторы: операторы — это очень распространенный синтаксис в JavaScript.
// 典型的for 循环语句
for (var i = 0; i < 7; i++) {
console.log(i);
}
Выражения:Выражение — это набор кодов, которые возвращают значение, выражение — еще один очень распространенный синтаксис, функциональное выражение — типичное выражение, если вы не понимаете, что такое выражение,MDNВыше есть подробное объяснение.
// 函数表达式
var add = function(a, b) {
return a + b
}
Объявления: Объявления делятся на объявления переменных и объявления функций.Примеры выражений функций в выражениях записываются следующим образом.
// 函数声明
function add(a, b) {
return a + b
}
Вы можете немного запутаться, чтобы прояснить отношения, давайте возьмем следующий код в качестве примера для интерпретации
// 函数表达式
var add = function(a, b) {
return a + b
}
Прежде всего, общая суть этого кода — объявление переменной (VariableDeclarator):
И переменная объявляется как функциональное выражение (FunctionExpression):
Оператор блока (BlockStatement) заключен в фигурные скобки в функциональном выражении:
Частью возврата внутри оператора блока является оператор возврата (ReturnStatement):
А return на самом деле является бинарным оператором или бинарным выражением (BinaryExpression):
Некоторые из упомянутых выше являются выражениями, некоторые — объявлениями, а некоторые — операторами, и, конечно же, есть многие другие, которые мы не упомянули, которые после анализа называются AST (абстрактное синтаксическое дерево).
Когда мы проводим синтаксический анализ, идея аналогична. Нам нужно проанализировать, какой уровень токена принадлежит выражению или оператору. Если это оператор, то это блочный оператор (BlockStatement) или циклы. Если это циклы, он принадлежит цикл while (WhileStatement) или цикл for (ForStatement) и т. д., в которых неизбежно рассмотрение масштаба проблемы, поэтому в этом также отражается сложность синтаксического анализа.
const parser = tokens => {
// 声明一个全时指针,它会一直存在
let current = -1;
// 声明一个暂存栈,用于存放临时指针
const tem = [];
// 指针指向的当前token
let token = tokens[current];
const parseDeclarations = () => {
// 暂存当前指针
setTem();
// 指针后移
next();
// 如果字符为'const'可见是一个声明
if (token.type === 'identifier' && token.value === 'const') {
const declarations = {
type: 'VariableDeclaration',
kind: token.value
};
next();
// const 后面要跟变量的,如果不是则报错
if (token.type !== 'identifier') {
throw new Error('Expected Variable after const');
}
// 我们获取到了变量名称
declarations.identifierName = token.value;
next();
// 如果跟着 '=' 那么后面应该是个表达式或者常量之类的,额外判断的代码就忽略了,直接解析函数表达式
if (token.type === 'operator' && token.value === '=') {
declarations.init = parseFunctionExpression();
}
return declarations;
}
};
const parseFunctionExpression = () => {
next();
let init;
// 如果 '=' 后面跟着括号或者字符那基本判断是一个表达式
if (
(token.type === 'parens' && token.value === '(') ||
token.type === 'identifier'
) {
setTem();
next();
while (token.type === 'identifier' || token.type === ',') {
next();
}
// 如果括号后跟着箭头,那么判断是箭头函数表达式
if (token.type === 'parens' && token.value === ')') {
next();
if (token.type === 'ArrowFunctionExpression') {
init = {
type: 'ArrowFunctionExpression',
params: [],
body: {}
};
backTem();
// 解析箭头函数的参数
init.params = parseParams();
// 解析箭头函数的函数主体
init.body = parseExpression();
} else {
backTem();
}
}
}
return init;
};
const parseParams = () => {
const params = [];
if (token.type === 'parens' && token.value === '(') {
next();
while (token.type !== 'parens' && token.value !== ')') {
if (token.type === 'identifier') {
params.push({
type: token.type,
identifierName: token.value
});
}
next();
}
}
return params;
};
const parseExpression = () => {
next();
let body;
while (token.type === 'ArrowFunctionExpression') {
next();
}
// 如果以(开头或者变量开头说明不是 BlockStatement,我们以二元表达式来解析
if (token.type === 'identifier') {
body = {
type: 'BinaryExpression',
left: {
type: 'identifier',
identifierName: token.value
},
operator: '',
right: {
type: '',
identifierName: ''
}
};
next();
if (token.type === 'operator') {
body.operator = token.value;
}
next();
if (token.type === 'identifier') {
body.right = {
type: 'identifier',
identifierName: token.value
};
}
}
return body;
};
// 指针后移的函数
const next = () => {
do {
++current;
token = tokens[current]
? tokens[current]
: { type: 'eof', value: '' };
} while (token.type === 'whitespace');
};
// 指针暂存的函数
const setTem = () => {
tem.push(current);
};
// 指针回退的函数
const backTem = () => {
current = tem.pop();
token = tokens[current];
};
const ast = {
type: 'Program',
body: []
};
while (current < tokens.length) {
const statement = parseDeclarations();
if (!statement) {
break;
}
ast.body.push(statement);
}
return ast;
};
До сих пор мыНасилиеАнализатор анализирует поток токенов и, наконец, получает грубое абстрактное синтаксическое дерево:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"identifierName": "add",
"init": {
"type": "ArrowFunctionExpression",
"params": [
{
"type": "identifier",
"identifierName": "a"
},
{
"type": "identifier",
"identifierName": "b"
}
],
"body": {
"type": "BinaryExpression",
"left": {
"type": "identifier",
"identifierName": "a"
},
"operator": "+",
"right": {
"type": "identifier",
"identifierName": "b"
}
}
}
}
]
}
2 Преобразование кода
2.1 Как преобразовать код?
В Babel наиболее часто используемое нашими пользователями место — это преобразование кода.Часто используемые плагины Babel создаются путем определения правил преобразования кода, а Babel в основном отвечает за разбор и генерацию кода.
Например, мы хотим использовать Babel в качестве преобразователя React в апплет.Грубая ситуация рабочего процесса Babel выглядит следующим образом:
- babel разбирает код React в абстрактное синтаксическое дерево
- Разработчики используют подключаемый модуль babel для определения правил преобразования и создания нового абстрактного синтаксического дерева, которое соответствует правилам апплета на основе исходного абстрактного синтаксического дерева.
- Babel генерирует код в соответствии с новым абстрактным синтаксическим деревом, и код в это время является новым кодом, соответствующим правилам апплета.
НапримерTaroЭто небольшое преобразование синтаксиса программы, выполненное Babel.
На этом этапе все поймут, что ключом к нашему преобразованию кода является создание нового абстрактного синтаксического дерева в соответствии с текущим абстрактным синтаксическим деревом и правилами, которые мы определяем.Процесс преобразования — это процесс создания нового абстрактного синтаксического дерева.
2.2 Обход абстрактного синтаксического дерева (реализовать обходчик обходчика)
Абстрактное синтаксическое дерево представляет собой древовидную структуру данных.Если мы хотим сгенерировать новое синтаксическое дерево, мы должны посетить узлы в AST, поэтому нам нужен инструмент для обхода узлов абстрактного синтаксического дерева.
const traverser = (ast, visitor) => {
// 如果节点是数组那么遍历数组
const traverseArray = (array, parent) => {
array.forEach((child) => {
traverseNode(child, parent);
});
};
// 遍历 ast 节点
const traverseNode = (node, parent) => {
const method = visitor[node.type];
if (method) {
method(node, parent);
}
switch (node.type) {
case 'Program':
traverseArray(node.body, node);
break;
case 'VariableDeclaration':
traverseArray(node.init.params, node.init);
break;
case 'identifier':
break;
default:
throw new TypeError(node.type);
}
};
traverseNode(ast, null);
};
2.3 Код преобразования (реализующий преобразователь трансформатора)
Код, который мы хотим преобразоватьconst add = (a, b) => a + b
На самом деле это объявление переменной, и само собой понятно, что код, который мы хотим преобразовать в es5, также должен быть объявлением переменной, например:
var add = function(a, b) {
return a + b
}
Конечно, вы также можете сгенерировать объявление функции напрямую, не следуя правилам, например:
function add(a, b) {
return a + b
}
На этот раз мы преобразуем код в объявление функции es5.
Наш предыдущий итераторtraverser
Получает два параметра: один — объект узла ast, а другой — посетитель. По сути, посетитель — это объект JavaScript, который монтирует различные методы. Посетитель также называется посетителем. Как следует из названия, он посетит каждый узел на ast , а затем используйте соответствующий метод для выполнения различных преобразований.
const transformer = (ast) => {
// 新 ast
const newAst = {
type: 'Program',
body: []
};
// 在老 ast 上加一个指针指向新 ast
ast._context = newAst.body;
traverser(ast, {
// 对于变量声明的处理方法
VariableDeclaration: (node, parent) => {
let functionDeclaration = {
params: []
};
if (node.init.type === 'ArrowFunctionExpression') {
functionDeclaration.type = 'FunctionDeclaration';
functionDeclaration.identifierName = node.identifierName;
}
if (node.init.body.type === 'BinaryExpression') {
functionDeclaration.body = {
type: 'BlockStatement',
body: [{
type: 'ReturnStatement',
argument: node.init.body
}],
};
}
parent._context.push(functionDeclaration);
},
//对于字符的处理方法
identifier: (node, parent) => {
if (parent.type === 'ArrowFunctionExpression') {
// 忽略我这暴力的操作....领略大意即可..
ast._context[0].params.push({
type: 'identifier',
identifierName: node.identifierName
});
}
}
});
return newAst;
};
3 Сгенерировать код (внедрить генератор-генератор)
Как мы упоминали ранее, шаг генерации кода фактически состоит в создании нового кода на основе нашего преобразованного абстрактного синтаксического дерева.Мы реализуем функцию, которая принимает объект (ast) и рекурсивно генерирует окончательный код.
const generator = (node) => {
switch (node.type) {
// 如果是 `Program` 结点,那么我们会遍历它的 `body` 属性中的每一个结点,并且递归地
// 对这些结点再次调用 codeGenerator,再把结果打印进入新的一行中。
case 'Program':
return node.body.map(generator)
.join('\n');
// 如果是FunctionDeclaration我们分别遍历调用其参数数组以及调用其 body 的属性
case 'FunctionDeclaration':
return 'function' + ' ' + node.identifierName + '(' + node.params.map(generator) + ')' + ' ' + generator(node.body);
// 对于 `Identifiers` 我们只是返回 `node` 的 identifierName
case 'identifier':
return node.identifierName;
// 如果是BlockStatement我们遍历调用其body数组
case 'BlockStatement':
return '{' + node.body.map(generator) + '}';
// 如果是ReturnStatement我们调用其 argument 的属性
case 'ReturnStatement':
return 'return' + ' ' + generator(node.argument);
// 如果是ReturnStatement我们调用其左右节点并拼接
case 'BinaryExpression':
return generator(node.left) + ' ' + node.operator + ' ' + generator(node.right);
// 没有符合的则报错
default:
throw new TypeError(node.type);
}
};
На данный момент мы завершили рудиментарный миниатюрный бабел и начали экспериментировать:
const compiler = (input) => {
const tokens = tokenizer(input);
const ast = parser(tokens);
const newAst = transformer(ast);
const output = generator(newAst);
return output;
};
const str = 'const add = (a, b) => a + b';
const result = compiler(str);
console.log(result);
// function add(a,b) {return a + b}
Мы успешно преобразовали стрелочную функцию es6 в функциональную функцию es5.
наконец
Мы можем понять принцип работы Вавила через этот миниатюрный бабел. Если вы заинтересованы в принципе компиляции и пойти глубже, это лучше. Пакет коллекции Babel является огромным проектом с сотнями тысяч линий кода. Мы используем только Немногие сотен линий кода. Это может показать свои самые основные принципы. Есть много необоснованных вещей в коде. Если вы хотите действительно понять, что Babel, вы можете прочитать его.исходный код.
Есть много вещей, связанных с принципом компиляции, которые может использовать внешний интерфейс.В дополнение к нашему обычному инструменту преобразования es6 babel, обнаружению кода eslint и т. д. мы также можем:
- Мини-программа Multi-End Escape Taro
- Горячее обновление мини-программыjs-интерпретатор
- Babel и мониторинг ошибокМониторинг исключений JavaScript на стороне браузера
- шаблонизатор
- css предварительная обработка, постобработка и т. д.
- ...
Эта статья подлежитthe-super-tiny-compilerВдохновленный.