1.需求分析
- 实现JSX语法转成JS语法的编译器
- 需求:将一段
JSX
语法的代码生成一个AST
,并支持遍历和修改这个AST
,将AST
重新生成JS语法的代码
JSX代码
<h1 id="title"><span>hello</span>world</h1>
JS代码
React.createElement("h1", {
id: "title"
},React.createElement("span", null, "hello"), "world");
2.编译器工作流
- 解析(Parsing) 解析是将最初原始的代码转换为一种更为抽象的表示(AST抽象语法树)
- 转换(Transformation)转换时将对这个对象的表示(AST)做一些处理,让它能做到编译器期望它做的事情
- 代码生成(Code Generation)接收处理之后的代码表示,然后将它转换成新的代码
2.1解析
解析一般分为两个阶段,词法分析(Lexical Analysis)和语法分析 (Syntactic Analysis)
- 词法分析 接收原始的代码,然后把他分割成一些被称为token的片段,这个过程是再词法分析器中完成的(Tokenizer或Lexer),使用状态机分词
- Token是一个数组,由一些代码语句的碎片组成,它们可以是数字、标签、标点符号、运算符或其他东西组成
- 语法分析接收之前生成的token,把他们转换成一种抽象的表示,这种表述描述了代码语句中的每一个片段以及他们之间的关系。这被称为中间表示,或者抽象语法树(Abatract Syntas Tree,缩写AST)
- 抽象语法树是一个嵌套程度很深的对象,用一种更容易处理的方式代表了代码本身,也能给我们更多信息
原始 jsx 代码
<h1 id="title"><span>hello</span>world</h1>
AST和tokens
Module {
type: 'Program',
body: [
ExpressionStatement {
type: 'ExpressionStatement',
expression: [JSXElement]
}
],
sourceType: 'module',
tokens: [
{ type: 'Punctuator', value: '<' },
{ type: 'JSXIdentifier', value: 'h1' },
{ type: 'JSXIdentifier', value: 'id' },
{ type: 'Punctuator', value: '=' },
{ type: 'String', value: '"title"' },
{ type: 'Punctuator', value: '>' },
{ type: 'Punctuator', value: '<' },
{ type: 'JSXIdentifier', value: 'span' },
{ type: 'Punctuator', value: '>' },
{ type: 'JSXText', value: 'hello' },
{ type: 'Punctuator', value: '<' },
{ type: 'Punctuator', value: '/' },
{ type: 'JSXIdentifier', value: 'span' },
{ type: 'Punctuator', value: '>' },
{ type: 'JSXText', value: 'world' },
{ type: 'Punctuator', value: '<' },
{ type: 'Punctuator', value: '/' },
{ type: 'JSXIdentifier', value: 'h1' },
{ type: 'Punctuator', value: '>' }
]
}
2.2 遍历(Traversal)
- 为了能处理所有的结点,我们需要遍历它们,使用的是深度优先遍历
- 对于上面的
AST
的遍历流程是这样的
代码实现
let esprima = require('esprima'); // AST转换工具
let estraverse = require('estraverse-fb'); // 深度优先遍历工具
let sourceCode = `<h1 id="title"><span>hello</span>world</h1>`;
// jsx解析为AST,支持jsx,带token
let ast = esprima.parseModule(sourceCode,{jsx:true,tokens:true});
console.log(ast);
let ident = 0; // 缩进
function padding(){
return ' '.repeat(ident);
}
//visitor访问者 访问器 深度优先遍历
estraverse.traverse(ast,{ // 访问器
enter(node){
console.log(padding()+node.type+'进入');
ident+=2;
},
leave(node){
ident-=2;
console.log(padding()+node.type+'离开');
}
});
访问器 运行代码输出结果
Program进入
ExpressionStatement进入
JSXElement进入
JSXOpeningElement进入
JSXIdentifier进入
JSXIdentifier离开
JSXAttribute进入
JSXIdentifier进入
JSXIdentifier离开
Literal进入
Literal离开
JSXAttribute离开
JSXOpeningElement离开
JSXClosingElement进入
JSXIdentifier进入
JSXIdentifier离开
JSXClosingElement离开
JSXElement进入
JSXOpeningElement进入
JSXIdentifier进入
JSXIdentifier离开
JSXOpeningElement离开
JSXClosingElement进入
JSXIdentifier进入
JSXIdentifier离开
JSXClosingElement离开
JSXText进入
JSXText离开
JSXElement离开
JSXText进入
JSXText离开
JSXElement离开
ExpressionStatement离开
Program离开
2.3 转换(Transformation)
- 编译器的下一步就是转换,它只是把 AST 拿过来然后对它做一些修改.它可以在同种语言下操作 AST,也可以把 AST 翻译成全新的语言
- 你或许注意到了我们的
AST
中有很多相似的元素,这些元素都有type
属性,它们被称为AST
结点。这些结点含有若干属性,可以用于描述 AST 的部分信息 - 当转换 AST 的时候我们可以添加、移动、替代这些结点,也可以根据现有的 AST 生成一个全新的 AST
- 既然我们编译器的目标是把输入的代码转换为一种新的语言,所以我们将会着重于产生一个针对新语言的全新的 AST
2.4 代码生成(Code Generation)
- 编译器的最后一个阶段是代码生成,这个阶段做的事情有时候会和转换(transformation)重叠,但是代码生成最主要的部分还是根据 AST 来输出代码
- 代码生成有几种不同的工作方式,有些编译器将会重用之前生成的 token,有些会创建独立的代码表示,以便于线性地输出代码。但是接下来我们还是着重于使用之前生成好的
AST
- 我们的代码生成器需要知道如何
打印
AST 中所有类型的结点,然后它会递归地调用自身,直到所有代码都被打印到一个很长的字符串中