实际上,不同用途的编译器之间可能会存在非常大的差异。它们唯一的共同点是,都会将代码转换成目标代码。
为Vue.js的模板构造AST是一件很简单的事。HTML是一种标记语言,它的格式非常固定。标签元素之间天然嵌套,形成父子关系。因此,一棵用于描述HTML的AST将拥有与HTML标签非常相似的树型结构。举例来说,假设有如下模板:
<div><p>Vue</p><p>Template</p></div>
我们可以将这段模板对应的AST设计为:
const ast = {
// AST的逻辑根节点
type: 'Root',
children: [
{
type: 'Element',
tag: 'div',
children: [
//div 节点的第一个子节点 p
{
type: 'Element',
tag: 'p',
children: [
{
type: 'Text',
tag: 'Vue',
}
]
},
//div 节点的第二个子节点 p
{
type: 'Element',
tag: 'p',
children: [
{
type: 'Text',
tag: 'Template',
}
]
},
]
}
]
}
可看到,AST在结构上与模板是“同构”的,它们都具有树型结构,
了解了AST的结构,接下来的任务是,使用程序根据模板解析后生成的Token构造出这样一棵AST。
首先,使用 tokenize 函数将本节开头给出的模板进行标记化。解析这段模板得到的 tokens如下所示:
const tokens= tokenize( `<div><p>Vue</p><p>Template</p></div>`)
执行上面这段代码,我们将得到如下 tokens:
const tokens = [
{type: "tag",name: "div"},
{type: "tag",name: "p"},
{type: "text",name: "Vue"},
{type: "tagEnd",name: "p"},
{type: "tag",name: "p"},
{type: "text",name: "Template"},
{type: "tagEnd",name: "p"},
{type: "tagEnd",name: "div"},
]
根据Token列表构建AST的过程,其实就是对 Token列表进行扫描的过程。从第一个Token开始,顺序地扫描整个 Token列表,直到列表中的所有 Token处理完毕。在这个过程中,我们要维护一个栈elementstack,这个栈将用于维护元素间的父子关系。每遇到一个开始标签节点,就构造一个Element类型的AST节点,并将其压人栈中。类似地,每当遇到一个结束标签节点,我们就将当前栈顶的节点弹出。这样,栈顶的节点将始终充当父节点的角色。扫描过程中到的所有节点,都会作为当前栈顶节点的子节点,并添加到栈顶节点的 children 属性下。
扫描Token列表并构建AST的具体实现如下:
//parse函数接收模板作为参数
function parse(str){
// 首先对模板进行标记化,得到 tokens
const tokens = tokenize(str)
//创建Root根节点
const root = {
type: 'Root',
children: []
}
//创建 elementStack 栈,起初只有Root 根节点
const elementStack = [root]
// 开启一个 while循环扫描 tokens,直到所有 Token 都被扫描完毕为止
while(tokens.length){
// 获取当前栈顶节点作为父节点 parent
const parent = elementStack[elementStack.length - 1]
//当前扫描的 Token
const t = tokens[0]
switch(t.type){
case 'tag':
//如果当前 Token 是开始标签,则创建 ELement 类型的 AST 节点
const elementNode = {
type: 'Element',
tag: t.name,
children: []
}
// 将其添加到父级节点的 children 中
parent.children.push(elementNode)
// 将当前节点压入栈
elementStack.push(elementNode)
break
case 'text':
// 如果当前 Token 是文本,则创建 Text 类型的 AST 节点
const textNode = {
type: 'Text',
content: t.content
}
// 将其添加到父节点的 children 中
parent.children.push(textNode)
break
case 'tagEnd':
// 遇到结束标签,将栈顶节点弹出
elementStack.pop()
break
}
// 消费已经扫描过的 token
tokens.shift()
}
//最后返回AST
return root
}
这里只是展示思路,其实还有很多问题没有处理,后面会完善。