这是我参与更文挑战的第9天,活动详情查看: 更文挑战。
前言
通过前面的学习,我们了解了模板编译的三个模块。本文,我们将一起继续深入学习其中的编译器。
解析器的作用
我们只有将模板解析成 AST
后,才能基于 AST 做优化或者生成代码字符串,那么解析器是如何将模板解析成 AST 的呢?
举个简单的例子:
<div>
<p>{{name}}</p>
</div>
复制代码
转换成 AST 后的样子:
{
tag: "div",
type: 1,
staticRoot: false,
sttatic: false,
plain: true,
parent: undefined,
attrsList: [],
atttrsMap: {},
children: [
{
tag: "p",
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: {tag:"div",...},
atttrsList: [],
attrsMap: {},
children: [
{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}
]
}
]
}
复制代码
AST 并不是什么高深的东西,它也是使用 Javascript 中的普通对象来描述一个节点。每个对象中的属性都保存了节点所需的各种数据。比如,parent属性保存了父节点的描述对象,children属性是一个数组,里面保存了一些子节点的描述对象。再比如,type属性表示一个节点的类型等。当很多个独立的节点通过parent属性和children属性连在一起时,就变成了一个树,而这样一个用对象描述的节点树其实就是 AST。
内部运行原理
解析器内部也分了好几个子解析器,其中最主要的是HTML解析器。
HTML解析器的作用是解析HTML,它在解析HTML的过程中会不断触发各种钩子函数。这些钩子函数包括开始标签钩子函数
、结束标签钩子函数
、文本钩子函数
以及注释钩子函数
。
parseHTML(template, {
start(tag, attrs, unary, start, end) {
// 解析到标签的开始位置,触发该函数
},
end(tag, start, end){
// 解析标签的结束位置,触发该函数
},
chars(text, start, end){
// 解析道纯文本,触发该函数
},
comment(text, start, end){
// 解析到注释,触发该函数
}
})
复制代码
以一个简单例子为例:
<div><p>hello world</p></div>
复制代码
在解析上面的模板时,会依次触发 start(<div>
)、start(<p>
)、chars(hello world
)、end(</p>
)、end(</div>
)。
如果触发了 start 钩子,我们就创建一个元素节点
,加到栈的顶部。
为什么使用 栈 来存储节点?
因为栈
有 first-in-last-out
的特点,它的典型运用是括号匹配问题。用在这里,有两个好处:
- 校验元素匹配的问题。
- 当栈为空时,表明模板已经处理完毕。
start钩子——创建元素节点
const stack = []; // 临时存储节点
let currentParent; // 父节点
function createASTElement(tag, attrs, parent){
return {
type: 1,
tag,
attrsList: attrs,
parent,
children
}
}
parseHTML(template, {
start(tag, attrs, unary, start, end) {
let element = createASTElement(tag, attrs, currentParent);
currentParent = element;
stack.push(element)
},
})
复制代码
end钩子——弹出元素节点
parseHTML(template, {
end(tag, start, end) {
const element = stack[stack.length-1];
stack.length -= 1;
currentParent = stack[statkc.length-1];
}
})
复制代码
chars———创建文本节点
parseHTML(template, {
chars(text, start, end){
let element = { type: 3, text };
let children = currentParent.chilren;
children.push(element);
}
})
复制代码
comment——创建注释节点
parseHTML(template, {
comment(text){
let element = { type: 3, text, isComment: true}
let children = currentParent.chilren;
children.push(element);
}
})
复制代码
HTML 解析器
运行原理
解析HTML简单来说就是用 HTML 模板字符串来循环,每轮循环都从 HTML 模板中截取一小段字符串,然后重复以上过程,直到 HTML 模板被截成一个空字符串时结束循环,解析完毕。
以之前的一个例子作为示例:
`<div>
<p>{{name}}</p>
</div>`
复制代码
- 第一轮循环,取出
<div>
,触发start
钩子函数,截取后的结果
`
<p>{{name}}</p>
</div>`
复制代码
- 第二轮循环,取出一段字符串,触发
chars
钩子函数
`
`
复制代码
截取后的结果为:
`<p>{{name}}</p>
</div>`
复制代码
- 第三轮循环,取出
<p>
,触发start
钩子函数,截取后的结果
`{{name}}</p>
</div>`
复制代码
- 第四轮循环,取出
{{name}}
字符串,触发chars
钩子函数,截取后的结果:
`</p>
</div>`
复制代码
- 第五轮循环,取出
</p>
,触发end
钩子函数,截取后的结果:
`
</div>`
复制代码
- 第六轮循环,取出一段字符串:
`
`
复制代码
截取后的结果:
`</div>`
复制代码
- 第七轮循环,取出
</div>
,触发end
钩子函数,截取后的结果:
``
复制代码
总结
本文,我们学习了解析器的基本工作流程,以及 HTML 解析器的基本原理。后面我们还会继续学习 HTML 解析器的具体实现,敬请期待…