1.编译器的概念
编译器其实就是一段程序,它用来将一种语言A翻译成另外一种语言B,我们称之为A为源代码,B为目标代码, 编译就是将源代码翻译为目标代码的过程。 完整的编译通常分为以下几个步骤,如图:
我们再vue模块中,通常就是组件模板的编译,而目标代码就是可以再浏览器运行的javaScript代码。
源代码:
<div>
<h1 :id="dynamicId"> Vue Template</h1>
</div>
目标代码:
function render(){
return h('div',{
h('h1',{id:dynamicId},'Vue Template')
})
}
我们可以看到Vue.js中编译器的目标代码其实就是生成渲染函数。那我们应该怎么样去生成渲染函数,这个就是我们需要思考的问题。
我们简单把生成渲染函数的步骤分为三步:
- 将模板字符串解析为模板AST的解析器(parse)
- 将模版的AST转换为JavaScript AST的转换器(transformer)
- 用JavaScript AST生成渲染函数代码(generator)
整体流程图如下:
2.parser的实现原理
解析器的入参就是字符串模板,解析会逐个读取字符串模板中的字符,并根据一定的规则将整个字符串切割为一个Token。
推荐一个AST很好的转换的地址:astexplorer.net/
我们举个简单的例子
<p>Vue</p>
我们理解上述的字符串模板为三个Token.
- 开始标签
- 文本节点:Vue
- 结束标签:
那解析器如何对模板进行分割呢?这个时候就得提到有限状态自动机。
- 状态总数是有限的 (每一个标签代表一个状态)
-
- 初始状态
- 标签开始状态 (<)
- 标签名称状态 (p)
- 文本状态 (Vue)
- 结束标签状态 (</)
- 结束标签名称状态 (p)
- .....
- 任一时刻,只处在一种状态之中
比如 <p>Vue</p> 从左往右,要么是 开始标签状态,要么是 文本状态,要么是 结束状态,不可能没有状态。
- 某种条件下,会从一种状态转变到另一种状态
一开始为开始状态,然后切换到文本状态或者其他状态。即 我们一定是在某一前提条件下,由某一状态切换到另一个状态
那我们理解了上面的东西,按照有限状态自动机的状态迁移过程,我们可以很容易地编写对应的代码实现。因此,有限状态自动机可以帮助我们完成对模板的标记化(tokenized),最终我们将得到一系列 Token。
状态机的状态
// 定义状态机的状态
const State = {
initial: 1, // 初始状态
tagOpen: 2, // 标签开始状态
tagName: 3, // 标签名称状态
text: 4, // 文本状态
tagEnd: 5, // 结束标签状态
tagName:6 //结束状态名称状态
}
匹配状态tokenize
function isAlpha(char) {
return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
}
function tokenize(str) {
let currentState = State.initial
const chars = []
const tokens = []
while(str) {
const char = str[0]
switch (currentState) {
case State.initial:
if (char === '<') {
currentState = State.tagOpen
str = str.slice(1)
} else if (isAlpha(char)) {
currentState = State.text
chars.push(char)
str = str.slice(1)
}
break
case State.tagOpen:
if (isAlpha(char)) {
currentState = State.tagName
chars.push(char)
str = str.slice(1)
} else if (char === '/') {
currentState = State.tagEnd
str = str.slice(1)
}
break
case State.tagName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tag',
name: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
case State.text:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '<') {
currentState = State.tagOpen
tokens.push({
type: 'text',
content: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
case State.tagEnd:
if (isAlpha(char)) {
currentState = State.tagEndName
chars.push(char)
str = str.slice(1)
}
break
case State.tagEndName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tagEnd',
name: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
}
}
return tokens
}
通过tokenize,我们可以通过下面三个Token
const tokens = tokenize(`<p>Vue</p>`)
//得到结果
tokens = [
{ type: 'tag', name: 'p' }, // 开始标签
{ type: 'text', content: 'Vue' }, // 文本节点
{ type: 'tagEnd', name: 'p' } // 结束标签
]
总而言之,通过有限自动机,我们能够将模板解析为一个个Token,进而可以用它们构建一棵 AST 了。但在具体构建 AST 之前,我们需要思考能否简化 tokenize 函数的代码。实际上,我们可以通过正则表达式来精简 tokenize 函数的代码。上文之所以没有从最开始就采用正则表达式来实现,是因为正则表达式的本质就是有限自动机。当你编写正则表达式的时候,其实就是在编写有限自动机。