编译器,是一种电脑程序,会将用某种编程语言写成的源代码,转换成另一种编程语言
最有特点的编译器:babel
编译器的基本思路(compiler)
- 对
template模板进行词法和语法分析,生成AST (抽象语法树) AST转换成附有JS语义的JavaScript AST- 解析
JavaScript AST生成代码
解析 template 生成 AST
词法分析
将文本分割成一个个的“token”
let x =1;
//let、x、=、1、;、
词法分析生成token的办法有2种:
- 使用正则进行词法分析
需要写大量的正则表达式,正则之间还有冲突需要处理,不容易维护,性能不高,所以正则只适合一些简单的模板语法,真正复杂的语言并不合适。 并且有的语言并不一定自带正则引擎。 - 使用有限状态自动机进行词法分析
在给定有限个输入情况下,将状态转移到最终状态
语法分析
就是将token结合定义语言的语法规格来得到一颗抽象语法树
例子
一个简单的模版:
<template>
<!-- 这是一段注释 -->
<p>{{ msg }}</p>
</template>
这个模板经过解析template生成的 AST 如下:
{ // type 字段,用来标记 AST 节点的类型
"type": 0, // 表示根节点
"children": [
{
"type": 3, // 表示注释节点
// 补充:若为 2 表示文本节点
// 针对源代码起点位置的字符不是 < 或者 {{ 的,当做是 文本节点 处理
"content": " 这是一段注释 ",
"loc": { // loc 代表的是节点对应的代码相关信息,包括代码的起始位置等等
"start": {
"column": 3,
"line": 2,
"offset": 3
},
"end": {
"column": 18,
"line": 2,
"offset": 18
},
"source": "<!-- 这是一段注释 -->"
}
},
{
"type": 1, // 表示元素节点
"ns": 0,
"tag": "p",
"tagType": 0,
"props": [], // props 描述的是节点的属性
"isSelfClosing": false,
"children": [
{
"type": 5, //表示插值节点
// 因为当遇到字符串 {{msg}} 的时候,会把当前代码当做是 插值节点 来解析
"content": {
"type": 4, // 指表达式
"isStatic": false,
"constType": 0,
"content": "msg",
"loc": {
"start": {
"column": 9,
"line": 3,
"offset": 27
},
"end": {
"column": 12,
"line": 3,
"offset": 30
},
"source": "msg"
}
},
"loc": {
"start": {
"column": 6,
"line": 3,
"offset": 24
},
"end": {
"column": 15,
"line": 3,
"offset": 33
},
"source": "{{ msg }}"
}
}
],
"loc": {
"start": {
"column": 3,
"line": 3,
"offset": 21
},
"end": {
"column": 19,
"line": 3,
"offset": 37
},
"source": "<p>{{ msg }}</p>"
}
}
],
"helpers": [],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 1,
"line": 4,
"offset": 38
},
"source": "\n <!-- 这是一段注释 -->\n <p>{{ msg }}</p>\n"
}
}
代码转换(Transform)
在得到AST后,我们一般会先将AST转为另一种AST(eg:JS AST),目的是生成更符合预期的AST,这一步称为代码转换。
transform 的目标是为了生成 JavaScript AST。因为渲染函数是一堆 js 代码构成的,编译器最终产物就是渲染函数,所以理想中的 AST 应该是用来描述渲染函数的 JS 代码。
代码生成
编译器的最后阶段是代码生成。有时编译器会做与转换重叠的东⻄,但大部分是代码生成只是意味着取出我们的 AST 和字符串化代码。
实现一个简单的编译器(基础的compiler)
大多数编译器分为三个主要阶段:解析、转换 和代码生成
- 解析 将原始代码转化为更抽象的代码
- 转换 采用这种抽象表示(见如下示例)并进行操作
- 代码生成 采用转换后的代码表示,并将其转换为新代码。
示例:
| LISP | C | |
|---|---|---|
| 2 + 2 | (add 2 2) | add (2, 2) |
| 4 - 2 | (subtract 4 2) | subtract (4, 2) |
| 2 + (4 - 2) | (add 2 (subtract 4 2)) | add (2, subtract (4, 2)) |
编译器将一种语言转换成另一种语言的过程其实就是示例中 LISP -> C 的过程
词法分析
//compiler.js
function tokenizer(input){
//其实我们的输入就是一段字符串,那我们要拿到token的话,遍历字符串就好
int current = 0; // 下标
int tokens = [];
while(current < input.length){
let char = input[current];
if(char === '('){
tokens.push({
type:'paren',
value:'(',
});
current++;
continue;
}
if(char === ')'){
tokens.push({
type:'paren',
value:')',
});
current++;
continue;
}
let WHITESPACE = /\s/; // 空格就跳过
if(WHITESPACE.test(char)){
current++;
continue;
}
let NUMBERS = /[0-9]/; // 处理数字
if(NUMBERS.test(char)){
let value = ''; // 存储数值
//遇到数字时开始收录直至遇到一个不是数字的字符为止
while(NUMBERS.test(char)){
value += char;
char = input[++current]; // 获取下一个char
}
//这时就得到一个完整的数值
tokens.push({
type: 'number',
value,
});
continue;
}
if(char === '"'){
let value = '';
char = input[++current]; // 当前元素位于‘"’的下一位
//获取引号中间的值
while(char !=='"'){
value += char;
char = input[++current];
}
char = input[++current]; // 跳过下一个引号
tokens.push({
type: 'string',
value,
});
continue;
}
let LEFTERS = /[a-z]/i;
if(LEFTERS.test(char)){
let value = '';
//获取操作符 add、subtract...
while(LEFTERS.test(char)){
value += char;
char = input[++current];
}
tokens.push({
type: 'name',
value,
});
continue;
}
throe new TypeError('I dont know what this character is: ' + char);
}
return tokens;
}
moudle.exports = {
tokenizer,
}
语法分析
//compiler.js
function parser(tokens){
let current = 0;
function walk(){
let token = tokens[current];
if(token.type === 'number'){
current++;
return {
type: 'NumberLiteral', //数字自变量
value: token.value,
};
}
if (token.type === 'string') {
current++;
return {
type: 'StringLiteral',
value: token.value,
};
}
//通过括号表达层级结构
if (token.type === 'paren' && token.value === '(') { // 先匹配左括号
token = tokens[++current];
let node = { //定义节点
type: 'CallExpression',
name: token.value,
params: [],
};
//下一个元素
token = tokens[++current];
while (token.type !== 'paren' || (token.type === 'paren' && token.value !== ')')) {
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
throw new TypeError(token.type);
}
let ast = {
type: 'Program',
body: [],
};
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}
moudle.exports = {
tokenizer,
parser,
}
代码转换
//compiler.js
//visitor模式——观察者模式
function traverser(ast, visitor) {
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
function traverseNode(node, parent) {
let methods = visitor[node.type];
if (methods && methods.enter) {
methods.enter(node, parent);
}
switch (node.type) {
case 'Program':
traverseArray(node.body, node);
break;
case 'CallExpression':
traverseArray(node.params, node);
break;
case 'NumberLiteral':
case 'StringLiteral':
break;
default:
throw new TypeError(node.type);
}
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
traverseNode(ast, null);
}
function transformer(ast) {
let newAst = {
type: 'Program',
body: [],
};
ast._context = newAst.body;
traverser(ast, {
NumberLiteral: {
enter(node, parent) {
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
CallExpression: {
enter(node, parent) {
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
node._context = expression.arguments;
if (parent.type !== 'CallExpression') {
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
parent._context.push(expression);
},
},
});
return newAst;
}
moudle.exports = {
tokenizer,
parser,
transformer,
}
代码生成
//compiler.js
function codeGenerator(node) {
switch (node.type) {
case 'Program':
return node.body.map(codeGenerator).join('\n');
case 'ExpressionStatement':
return (
codeGenerator(node.expression) + ';' // << (...because we like tocode the *correct* way)
);
case 'CallExpression':
return codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')';
case 'Identifier':
return node.name;
case 'NumberLiteral':
return node.value;
case 'StringLiteral':
return '"' + node.value + '"';
default:
throw new TypeError(node.type);
}
}