一,前言
上篇,主要介绍了生成 ast 语法树-正则说明部分,涉及以下几个点:
- 介绍了html模板的解析原理;
- 详解源码中模板解析相关的7类正则;
- 匹配标签名
- 匹配命名空间标签名
- 匹配开始标签-开始部分
- 匹配结束标签
- 匹配属性
- 匹配开始标签-闭合部分
- 匹配插值表达式
本篇,生成 ast 语法树-代码实现
二,模板解析实现
模板解析的思路:对模板不停截取,直至全部解析完毕;
模板解析代码实现:在while循环,使用正则不断的对html模板中的有效信息进行匹配和截取;
1,判断是标签 or 文本?
上篇提到,判断是标签还是文本,主要是看内容的开头第一个字符是否为<尖角号:
- 如果是尖角号,说明是标签;
- 如果不是尖角号,说明是文本;
// src/compiler/index.js#parserHTML
function parserHTML(html) {
while(html){
// 解析标签or文本,判断html的第一个字符,是否为 < 尖角号
let index = html.indexOf('<');
if(index == 0){
console.log("是标签")
} else{
console.log("是文本")
}
}
}
输出结果:
2,解析开始标签-parseStartTag方法
包含尖叫号<的情况: 可能是开始标签 <div>,也可能是结束标签</div>;
所以,当解析到标签时,应先使用正则匹配开始标签;如果没有匹配成功,再使用结束标签进行匹配
parseStartTag方法:匹配开始标签,返回匹配结果,即标签名;
备注:匹配结果的索引 1 为标签名
// src/compiler/index.js#parserHTML
/**
* 匹配开始标签,返回匹配结果(开始标签名)
*/
function parseStartTag() {
// 匹配开始标签
const start = html.match(startTagOpen);
// 构造匹配结果对象:包含标签名和属性
const match = {
tagName:start[1], // 数组索引 1 为标签名
attrs:[]
}
console.log("match结果:" + match)
// todo 删除字符串中匹配完成的部分
}
function parserHTML(html) {
// 对模板不停做匹配和截取操作,直至全部解析完毕
while (html) {
// 解析标签和文本(看开头是否为 <)
let index = html.indexOf('<');
if (index == 0) {
console.log("解析 html:" + html + ",结果:是标签")
// 如果是标签,继续解析开始标签和属性
const startTagMatch = parseStartTag();// 匹配开始标签,返回匹配结果
// 1,匹配到开始标签:无需执行后续逻辑,直接进入下一次 while,继续解析后续内容
if (startTagMatch) {
continue;
}
// 2,未匹配到开始标签:此时有可能为结束标签</div>
// 如果匹配到结束标签,无需执行后续逻辑,直接进入下一次 while,继续解析后续内容
if (html.match(endTag)) {
continue;
}
} else {
console.log("解析 html:" + html + ",结果:是文本")
}
}
}
调试并查看开始标签的match结果:
3,截取匹配完成的部分-advance方法
开始标签解析完成后,需要将匹配完成的部分截掉,达到如下效果:
<!-- 解析前 -->
<div id=app>{{message}}</div>
<!-- 解析后:开始标签<div被截掉 -->
id=app>{{message}}</div>
为达到以上效果,创建advance(前进)方法:截取html内容至当前已解析的位置,即删除已处理完成的部分;
// src/compiler/index.js#parserHTML
function parserHTML(html) {
/**
* 截取字符串
* @param {*} len 截取长度
*/
function advance(len){
html = html.substring(len);
}
/**
* 匹配开始标签,返回匹配结果
*/
function parseStartTag() {
// 匹配开始标签,开始标签名为索引 1
const start = html.match(startTagOpen);
// 构造匹配结果,包含标签名和属性
const match = {
tagName:start[1],
attrs:[]
}
console.log("match 结果:" + match)
// 截取匹配到的结果
advance(start[0].length)
console.log("截取后的 html:" + html)
}
...略
}
调试并查看截取后的html片段:
4,解析开始标签中的属性-while循环
id="app">{{message}}</div>
在开始标签中,由于可能存在多个属性,因此这部分需要进行多次处理;
// src/compiler/index.js#parserHTML#parseStartTag
function parseStartTag() {
const start = html.match(startTagOpen);
const match = {
tagName: start[1],
attrs: []
}
console.log("match 结果:" + match)
// 截取匹配到的结果
advance(start[0].length)
console.log("截取后的 html:" + html)
// ******* 开始解析标签的属性 id="app" a=1 b=2>*******//
let end; // 是否匹配到开始标签的结束符号 > 或 />
let attr; // 存储属性匹配的结果
// 匹配并获取属性,放入 match.attrs 数组
// 例如 <div>标签:当匹配到字符 >,表示标签结束,不再继续匹配标签内的属性
// attr = html.match(attribute) 匹配属性并赋值当前属性的匹配结果
// !(end = html.match(startTagClose)) 没有匹配到开始标签的结束符号 > 或 />
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
// 将匹配到的属性记录到数组 match.attrs(属性对象包含属性名和属性值)
match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] })
// 截取掉已匹配完成的属性,如:xxx=xxx
advance(attr[0].length)
}
// 当匹配到开始标签的关闭符号 > 时,当前标签处理完成,while 结束
// 此时,<div id="app" 处理完成,需要连同关闭符号 > 一起截取掉
if (end) {
advance(end[0].length)
}
// 开始标签处理完成后,返回匹配结果:tagName 标签名 + attrs 属性对象
return match
}
备注:前面提到过,属性的值可能是
"xxx"、'xxx'、xxx三种之一,所以属性取值为attr[3] || attr[4] || attr[5],即取匹配结果中有值的一个即可;
5,总结:开始标签处理过程的详细说明
<div id="app" a=1 b=2>
对开始标签处理过程的每一步进行说明:
- 以
<开头,说明是标签;此时,可能是开始标签,也可能是结束标签; - 匹配正则
startTagOpen,获取属性名和属性; - 匹配
<div剩id="app" a=1 b=2> - 匹配
id="app"剩a=1 b=2> - 匹配
a=1剩b=2> - 匹配
b=2剩> - 匹配到
>时,匹配结束while循环终止`;
至此,开始标签就解析完成了
注意:对于
match的匹配结果,match[0]表示到的匹配内容,match[1]表示捕获到的内容;
6,抛出开始标签、结束标签及文本
todo:需要从流程上说明“发射”目的,或放到下一篇中;目前读到这里可能理解上会生硬一些;
继续,将开始标签的状态(开始标签、结束标签、文本标签)发射出去
编写三个发射状态的方法,分别用于向外发射开始标签、结束标签、文本标签
// src/compiler/index.js#parserHTML#start
// src/compiler/index.js#parserHTML#end
// src/compiler/index.js#parserHTML#text
// 开始标签
function start(tagName, attrs) {
console.log("start", tagName, attrs)
}
// 结束标签
function end(tagName) {
console.log("end", tagName)
}
// 文本标签
function text(chars) {
console.log("text", chars)
}
当匹配到开始标签、结束标签、文本时,将数据发送出去
// src/compiler/index.js#parserHTML#parseStartTag
function parserHTML(html) {
/**
* 匹配开始标签,返回匹配结果
*/
function parseStartTag() {
const start = html.match(startTagOpen);
if(start){
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length)
let end;
let attr;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] })
advance(attr[0].length)
}
if (end) {
advance(end[0].length)
}
return match; // 没有匹配到
}
return false;
}
while (html) {
let index = html.indexOf('<');
if (index == 0) {
console.log("解析 html:" + html + ",结果:是标签")
const startTagMatch = parseStartTag();
console.log("开始标签的匹配结果 startTagMatch = " + JSON.stringify(startTagMatch))
if (startTagMatch) {
// 匹配到开始标签,调用start方法,传递标签名和属性
start(startTagMatch.tagName, startTagMatch.attrs)
continue;
}
// 如果开始标签没有匹配到,有可能是结束标签 </div>
let endTagMatch;
if (endTagMatch = html.match(endTag)) {// 匹配到了,说明是结束标签
// 匹配到开始标签,调用 start 方法,向外传递标签名和属性
end(endTagMatch[1])
// 删除已匹配完成的部分
advance(endTagMatch[0].length)
continue;
}
}
// 如果是文本:将文本内容取出来并发射出去,并从html片段中截取掉
if(index > 0){
// 此时的 html 片段为:hello</div>
let chars = html.substring(0, index)
// 向外传递文本
text(chars);
// 删除已匹配完成的部分
advance(chars.length)
}
}
至此,通过对html模板的解析,已经获取到了模板中的标签名、属性等关键信息,后续再通过这些信息构建出AST语法树;
三,模板解析测试-复杂模板
<body>
<div id="app" a='1' b=2 > <p>{{message}} <span>Hello Vue</span></p></div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data() {
return { message: "Brave" }
},
});
</script>
</body>
输出结果:
进入 state.js - initData,数据初始化操作
***** 进入 $mount,el = #app*****
获取真实的元素,el = [object HTMLDivElement]
options 中没有 render , 继续取 template
options 中没有 template, 取 el.outerHTML = <div id="app" a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>
***** 进入 compileToFunction:将 template 编译为 render 函数 *****
***** 进入 parserHTML:将模板编译成 AST 语法树*****
解析 html:<div id="app" a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: <div id="app" a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>*****
html.match(startTagOpen) 结果:{"tagName":"div","attrs":[]}
截取匹配内容后的 html: id="app" a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配到属性 attr = [" id=\"app\"","id","=","app",null,null]
截取匹配内容后的 html: a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配到属性 attr = [" a=\"1\"","a","=","1",null,null]
截取匹配内容后的 html: b="2"> <p>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配到属性 attr = [" b=\"2\"","b","=","2",null,null]
截取匹配内容后的 html:> <p>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配关闭符号结果 html.match(startTagClose):[">",""]
截取匹配内容后的 html: <p>{{message}} <span>Hello Vue</span></p></div>
===============================
>>>>> 开始标签的匹配结果 startTagMatch = {"tagName":"div","attrs":[{"name":"id","value":"app"},{"name":"a","value":"1"},{"name":"b","value":"2"}]}
发射匹配到的开始标签-start,tagName = div,attrs = [{"name":"id","value":"app"},{"name":"a","value":"1"},{"name":"b","value":"2"}]
解析 html: <p>{{message}} <span>Hello Vue</span></p></div>,结果:是文本
发射匹配到的文本-text,chars =
截取匹配内容后的 html:<p>{{message}} <span>Hello Vue</span></p></div>
===============================
解析 html:<p>{{message}} <span>Hello Vue</span></p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: <p>{{message}} <span>Hello Vue</span></p></div>*****
html.match(startTagOpen) 结果:{"tagName":"p","attrs":[]}
截取匹配内容后的 html:>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配关闭符号结果 html.match(startTagClose):[">",""]
截取匹配内容后的 html:{{message}} <span>Hello Vue</span></p></div>
===============================
>>>>> 开始标签的匹配结果 startTagMatch = {"tagName":"p","attrs":[]}
发射匹配到的开始标签-start,tagName = p,attrs = []
解析 html:{{message}} <span>Hello Vue</span></p></div>,结果:是文本
发射匹配到的文本-text,chars = {{message}}
截取匹配内容后的 html:<span>Hello Vue</span></p></div>
===============================
解析 html:<span>Hello Vue</span></p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: <span>Hello Vue</span></p></div>*****
html.match(startTagOpen) 结果:{"tagName":"span","attrs":[]}
截取匹配内容后的 html:>Hello Vue</span></p></div>
===============================
匹配关闭符号结果 html.match(startTagClose):[">",""]
截取匹配内容后的 html:Hello Vue</span></p></div>
===============================
>>>>> 开始标签的匹配结果 startTagMatch = {"tagName":"span","attrs":[]}
发射匹配到的开始标签-start,tagName = span,attrs = []
解析 html:Hello Vue</span></p></div>,结果:是文本
发射匹配到的文本-text,chars = Hello Vue
截取匹配内容后的 html:</span></p></div>
===============================
解析 html:</span></p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: </span></p></div>*****
未匹配到开始标签,返回 false
===============================
发射匹配到的结束标签-end,tagName = span
截取匹配内容后的 html:</p></div>
===============================
解析 html:</p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: </p></div>*****
未匹配到开始标签,返回 false
===============================
发射匹配到的结束标签-end,tagName = p
截取匹配内容后的 html:</div>
===============================
解析 html:</div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: </div>*****
未匹配到开始标签,返回 false
===============================
发射匹配到的结束标签-end,tagName = div
截取匹配内容后的 html:
===============================
当前 template 模板,已全部解析完成
四,结尾
本篇,主要介绍了生成 ast 语法树 - 模板解析部分,主要涉及以下几个点:
使用正则对 html 模板进行解析和处理,匹配到模板中的标签和属性
- 解析开始标签-parseStartTag方法
- 截取匹配完成的部分-advance方法
- 解析开始标签中的属性-while循环
- 开始标签处理过程的详细说明
- 对开始标签、结束标签及文本的发射处理
下一篇,生成 ast 语法树 - 构造树形结构
维护日志
- 20230125:重新梳理文章目录结构,对模板解析过程进行详细说明,优化部分代码并添加必要注释;