什么是编译?
编译器将一段源代码转换成目标代码的过程叫作编译,也就是将“语言A”=>“语言B”的过程。
编译流程
一个完整的编译过程通常包括以下几个步骤:
- 词法分析
- 语法分析
- 中间代码生成
- 优化
- 目标代码生成
VUE模板编译器在编译转换过程中都做了哪些事呢?
与上述编译流程中不同的是,在vue的模板编译流程中,充当“源代码”角色的是组件模板,而“目标代码”则是一个JS渲染函数。
// 源代码
<div>
<p id='myApp' >source code</p>
</div>
//目标代码
function render(){
return h('div',[
h('p', { id: 'myApp', 'source code'})
])
}
编译器将模板转换成render函数,这个过程中编译器会对模板字符串进行词法分析和语法分析,得到模板AST,将模板AST转换成JavaScript AST最后生成渲染函数render()。
AST是什么?
抽象语法树(abstract syntax tree,AST) 是一种源代码抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,它并不依赖源代码的语法。
定义一个模板AST结构如下:
// 模板
<div>
<p id='myApp'>source code</p>
</div>
// 模板AST
const myAST = {
type: 'Root',
children: [
// div 标签节点
{
type: 'Element',
tag: 'div',
children: [
// p 标签节点
{
type: 'Element',
tag: 'p',
props: [
//属性节点
{
type: 'attrs',
name: 'id',
content: 'myApp'
}
],
children: [
// 文本节点
{
type: 'text',
content: 'source code'
}
]
}
]
}
]
}
以上的代码中我们可以看到
- 模板AST通常用不同的type属性来区分
- 标签节点的子节点存储在children数组中
- 节点上的属性存储在props数组中
那么我们是如何将一段模板代码转换成AST的呢?
想要得到模板AST并对其进行转换,需要经过编译流程中的词法分析和语法分析。要想实现这一过程,我们需要封装一个parser函数,在该函数中我们将会把模板字符串切割得到一个个token,再将token数组遍历构建AST树。
function parser(template){
//对模板字符串进行切割划分
const tokens = tokenInit(template)
// const tokens = [
// {type: "tag", name: "div"}, //div 开始标签
// {type: "tag", name: "p"}, //p 开始标签
// {type: "text", content: "source code"}, // 文件节点
// {type: "tagEnd", name: "p"}, // p 结束标签
// {type: "tagEnd", name: "div"} // div 结束标签
// ]
//创建根节点
const root = [
type: 'Root',
parent: null,
children: []
]
//创建一个栈,充当tokens和AST的中转容器
//将tokens数组中的元素挨个压入栈中,把栈顶元素当做父节点,
//对类型为‘tag’的节点作为子节点压入children中,遇到类型为‘tagEnd’就弹出当前栈顶元素,
//最后返回root根节点作为AST
const elementStack = [root]
while(tokens.length){
//elementStack栈的长度会不断改变,所以要动态的获取栈顶元素作为当前节点
const currentNode = elementStack[elementStack.length -1];
const currentToken = tokens[0]
switch (currentToken.type){
case 'tag':
//创建AST节点
const element = {
type: 'Element',
tag: currentToken.name,
children: []
}
//将节点压入当前栈顶元素的children中。如果是第一个节点,将当前节点压入root.children中
currentNode.children.push(element)
elementStack.push(currentNode)
break;
case 'text':
const test = {
type: 'Text',
content: currentNode.content
}
currentNode.children.push(text)
break;
case 'tagEnd':
//弹出当前节点
elementStack.pop()
break;
}
tokens.shift(); // 弹出当前token
}
//返回根节点AST,遍历结束后根节点中已经包含了全部节点
return root;
}
将模板AST转换成JavaScript AST
与上述的模板AST类似,我们将一个函数拆分,会发现一个函数可以分为以下几种类型:
- 函数名称
- 函数参数
- 函数体
JavaScript AST与模板 AST没有太大区别
function render(){
return h('div',[
h('p', { id: 'myApp', 'source code'})
])
}
// JavaScript AST结构
const FunctionNode = {
// 函数节点类型
type: 'Function',
id: {
type: 'Identifier',
//函数名称
name: 'render'
},
//函数的参数
params: [],
//函数体
body: [
{
type: 'ReturnStatement',
return: {
//回调函数类型
type: 'CallEXP',
callee: {
type: 'Identifier',
name: 'h'
},
//arguments参数
arguments: [
{
//字符串类型
type: 'StringEXP',
value: 'div'
},
{
//数组类型
type: 'ArrayEXP',
elements: [
{
type: 'CallEXP',
callee: {
type: 'Identifier',
name: 'h'
},
arguments: [
{
type: 'StringEXP',
value: 'p'
}
...
]
}
],
}
]
}
}
]
}
Vue编译器通过将JavaScript AST节点遍历递归解析转换成渲染函数。在这个代码生成的过程,其本质就是字符串拼接,具体不再做讨论。
参考
- Vue.js设计与实现