一,前言
上篇,主要介绍了 vue 数据渲染核心流程,涉及以下几个点:
- Vue 核心渲染流程回顾
- 三种模板写法及优先级
- 两种数据挂载方式
- Vue 的原型方法 $mount
- compileToFunction -> parserHTM流程说明
本篇,生成 ast 语法树-正则说明
二,模板解析
1,模板解析的说明
上文说到,compileToFunction方法是Vue模板编译的入口:
export function compileToFunction(template) {
// 1,将模板变成 AST 语法树
let ast = parserHTML(template);
// 2,根据 AST 语法树生成 render 函数
let code = generate(ast);
}
在上面的compileToFunction方法中,主要做了以下两件事:
- 将模板变成
AST语法树; - 根据
AST语法树生成render函数;
将html模板编译为ast语法树,就是用js对象的树形结构来描述HTML语法;
这里,需要对html模板进行解析,而解析的方式就是使用正则不断地进行匹配和处理;
构建 ast 语法树有很多可用插件,例如:html 的解析器 htmlparser2;
在
Vue中,由于存在自定义指令等框架特性功能,所以Vue的ast解析逻辑是定制实现的;
一个AST在线解析网站:astexplorer.net/
从示例中可以看出,ast抽象语法树就是使用js来描述html语法;
3,模板解析的方式
使用正则对html模板进行顺序解析和处理,每处理完一段,就将这部分截取掉,就这样不停的进行解析和截取,直至将整个模板全部解析完毕;
html模板示例:
<!-- start:从头开始,使用正则不断进行匹配和截取 -->
<!-- 1,解析开始标签,并截取删除 -->
<div>abcdefg<span></span></div> 开始标签:<div>
<!-- 2,解析文本内容,并截取删除 -->
abcdefg<span></span></div> 文本内容:abcdefg
<!-- 3,解析开始标签,并截取删除 -->
<span></span></div> 开始标签:<span>
<!-- 4,解析结束标签,并截取删除 -->
</span></div> 结束标签:</span>
<!-- 5,解析结束标签,并截取删除 -->
</div> 结束标签:</div>
<!-- end:全部匹配完成 -->
根据示例的逻辑地点,在parserHTML方法中,可以使用while循环对模板不停执行截取操作,直至全部解析完毕后,即可构建出ast语法树:
function parserHTML(html) {
while(html){
// todo: 解析 html 并构建 ast 语法树
}
}
还有一个问题,那就是:如何判断html的内容是“标签”还是“文本”?
方法:判断内容开头的第一个字符是否为尖角号< :
- 如果是尖角号,说明是标签;
- 如果不是尖角号,说明是文本
三,正则表达式说明
1,Vue2 正则相关说明
在Vue2源码中,使用正则对html标签和内容进行匹配并解析出结果,涉及相关正则如下:
// 标签名 a-aaa
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// 命名空间标签 aa:aa-xxx
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 开始标签-捕获标签名
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 结束标签-匹配标签结尾的 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// 匹配属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配标签结束的 >
const startTagClose = /^\s*(\/?)>/;
// 匹配 {{ }} 表达式
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
在正则解析开始之前,还需要了解一些前置知识点:
- 在字符串中,存在转译行为,
\具有转译含义;(以上正则都是字符串) - 字符串需要通过
new RegExp(xxx)才能转换为正则表达式对象; - 在正则表达式中,同样也存在转译行为,
\也具有转译含义; - 正则表达式的开始和结尾都被
/包裹;
js 正则可视化工具:jex.im/regulex
2,匹配标签名
标签说明
// 匹配标签名 aa-xxx
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
正则解析
1,字符串转译:
- 第一个
\:用来转译第二个\,所以第一组\\相当于\; - 第三个
\:用来转译第四个\,所以第二组\\也相当于\;
2,正则表达式转译:
- 字符串
\\-转译后,成为正则中的\-,正则中表示-; - 字符串
\\.转译后,成为正则中的\.,正则中表示.;
todo:vue标签可以是 xx-.xxx 吗?
测试匹配结果
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
let reg = new RegExp(ncname);
console.log(reg) // 输出转译后的正则: /[a-zA-Z_][\-\.0-9_a-zA-Z]*/
console.log(reg.test('a-aaa')) // 匹配结果 true
1,对比
ncname字符串和正则表达式,字符串中的/起到了转译作用;2,将字符串转为正则表达式对象,
new RegExp(ncname)结果为/[a-zA-Z_][\-\.0-9_a-zA-Z]*/:3,根据正则匹配结果,正则表达式中的
/也起到了转译作用;4,正则的开始和结尾都被
/包裹,如上转译后的正则表达式/[a-zA-Z_][\-\.0-9_a-zA-Z]*/
3,匹配命名空间标签
标签说明
// 命名空间标签:aa:aa-xxx
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
在
ncname标签名正则的基础上,实现了对命名空间标签的匹配;
正则解析
分为两部分来看
1,(?:${ncname}\\:)?
?:: 表示匹配,但不捕获;${ncname}: 标签名,参考ncname正则;\\:: 正则中的\:,表示后面有一个冒号;?: 最后的问号,表示前面的内容可有可无;比如:aa:可以没有
2,${ncname} 标签名,参考ncname正则;
- 用于匹配
aa:aa-xxx,这种标签叫做“命名空间标签”,很少会使用到;
测试匹配结果
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
let reg = new RegExp(qnameCapture);
//转译后:/((?:[a-zA-Z_][\-\.0-9_a-zA-Z]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Z]*)/
console.log(reg)
console.log(reg.test('aa:aa-xxx')) // 匹配结果 true
4,匹配开始标签-开始部分
标签说明
// 标签开头的正则 捕获的内容是标签名
const startTagOpen = new RegExp(`^<${qnameCapture}`);
//转译后:/^<((?:[a-zA-Z_][\-\.0-9_a-zA-Z]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Z]*)/
console.log(startTagOpen)
匹配标签名->匹配命名空间标签名->匹配开始标签
正则解析
在命名空间标签的正则基础之上,添加了标签开头的匹配;
^<:表示以<开头;
测试匹配结果
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
//转译后:/^<((?:[a-zA-Z_][\-\.0-9_a-zA-Z]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Z]*)/
console.log(startTagOpen)
console.log(startTagOpen.test('<aa:aa-xxx')) // 匹配结果 true
console.log('<aa:aa-xxx'.match(startTagOpen))
[
'<aa:aa-xxx',
'aa:aa-xxx', // 开始标签的标签名,数组索引 1
index: 0,
input: '<aa:aa-xxx',
groups: undefined
]
结果:匹配到开始标签,获得开始标签中的标签名;
match的匹配结果是一个数组,数组的第一项为匹配结果;
5,匹配结束标签
标签说明
// 匹配结束标签,如: </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// /^<\/((?:[a-zA-Z_][\-\.0-9_a-zA-Z]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Z]*)[^>]*>/
console.log(endTag)
正则解析
此正则分为 4 部分:
1,^<\\/:<符号开头,后面跟一个/,即以</开头;
2,${qnameCapture}:参考命名空间标签;
3,[^>]*:中间可以是任意数量不为>的字符;
4,>:最后一位必须是>;
测试匹配结果
console.log('</aa:aa-xxxdsadsa>'.match(endTag))
[
'</aa:aa-xxxdsadsa>',
'aa:aa-xxxdsadsa', // 结束标签的标签名,数组索引 1
index: 0,
input: '</aa:aa-xxxdsadsa>',
groups: undefined
]
结果:匹配到结束标签,获得结束标签中的标签名;
6,匹配属性
标签说明
// 匹配属性(索引 1 为属性 key、索引 3、4、5 其中一直为属性值):aaa="xxx"、aaa='xxx'、aaa=xxx
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
正则解析
按以下几个部分进行拆解:
^\s*:- 开头为 n 个空格(
0个或n个空格)
- 开头为 n 个空格(
[^\s"'<>\/=]+: 匹配属性名;- 不是空格、引号、尖脚号、等号的
n个字符;
- 不是空格、引号、尖脚号、等号的
?:\s*(=)\s*:- 空格和空格之间可以夹着一个等号
=;
- 空格和空格之间可以夹着一个等号
- ?:"([^"])"+|'([^'])'+|([^\s"'=<>`]+) :匹配属性值
"([^"]*)": 可能是双引号;'([^']*)': 可能是单引号;- [^\s"'=<>`]+: 不是空格、引号、尖脚号、等号的 n 个字符;
属性的 3 种情况与取值
- 情况 1:双引号的情况,如:
aaa="xxx"; - 情况 2:单引号的情况,如:
aaa='xxx'; - 情况 3:没引号的情况,如:
aaa=xxx;
// 情况 1:双引号的情况,aaa="xxx"
console.log('aaa="xxx"'.match(attribute))
[
'aaa="xxx"',
'aaa',
'=',
'xxx',
undefined,
undefined,
index: 0,
input: 'aaa="xxx"',
groups: undefined
]
// 此时,索引3是有值的(xxx),4、5是undefined
// 情况 2:单引号的情况,aaa='xxx',会匹配到下一个位置
console.log(`aaa='xxx'`.match(attribute))
[
"aaa='xxx'",
'aaa',
'=',
undefined,
'xxx',
undefined,
index: 0,
input: "aaa='xxx'",
groups: undefined
]
// 此时,会匹配到索引 4,即第二个位置
// 情况 3:没有引号的情况,aaa=xxx,第三个位置就是不带单引号的
console.log('aaa=xxx'.match(attribute))
[
'aaa=xxx',
'aaa',
'=',
undefined,
undefined,
'xxx',
index: 0,
input: 'aaa=xxx',
groups: undefined
]
// 索引 3、4 是 undefined,5 是有值的(xxx),表示匹配到了最后一位
// 属性名:[1]
// 属性值:[3] || [4] || [5]
综上 3 种情况:
- 属性的
key:取索引 1; - 属性的
value:取索引 3(双引号)、4(单引号)、5(无引号) 中,有值的那一个;
7,匹配开始标签-闭合部分
标签说明
// 匹配结束标签:>
const startTagClose = /^\s*(\/?)>/;
正则解析
^\s*: 空格 n 个(\/?)>: 尖角号有以下两种情况 -/>: 自闭合 ->: 没有/的闭合
8,匹配 {{ }} 插值表达式
标签说明
// 匹配 {{ xxx }} ,匹配到 xxx
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
在 Vue3 中是靠状态机来实现的,不会像 Vue2 有如此多正则表达式;
四,结尾
本篇,主要介绍了生成 ast 语法树-正则说明部分,涉及以下几个点:
- 介绍了html模板的解析原理;
- 详解源码中模板解析相关的7类正则;
- 匹配标签名
- 匹配命名空间标签名
- 匹配开始标签-开始部分
- 匹配结束标签
- 匹配属性
- 匹配开始标签-闭合部分
- 匹配插值表达式
下一篇,生成 ast 语法树-代码实现
维护日志:
- 20230119:添加内容中的代码高亮,微调目录结构和部分内容描述;
- 20230120:对每种正则优化了“正则解析”部分的描述,对每种正则添加了“测试匹配结果”部分;
- 20230124:添加正则前置知识点,重写“正则解析含义理解”部分,补充“正则匹配结果”的详细分析,添加必要注释说明;