写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧
咳咳,上一篇文章,我们已经大致把 parse 的流程给记录了一遍,如果没看过,比较建议,先把这个流程给看了
Compile - 源码版 之 Parse 主要流程
但是忽略了其中的处理细节,比如标签怎么解析的,属性怎么解析的,而且这两个内容也是非常多的,所以需要单独拎出来详细记录,不然混在一起,又臭又长
白话版在这~ Compile - 白话版
今天的内容是,记录 标签解析的 源码
首先,开篇之前呢,我们来了解一下文章会出现过的正则
相关正则
var ncname = '[a-zA-Z_][\\w\\-\\.]*';
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
var startTagOpen = new RegExp(("^<" + qnameCapture));
var startTagClose = /^\s*(\/?)>/;
var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
主要是四个
startTagOpen
匹配 头标签的 前半部分。当字符串开头是 头标签时,可以匹配
startTagClose
匹配 头标签的 右尖括号。当字符串开头是 > 时,可以匹配
endTag
匹配 尾标签。当字符串开头是 尾标签时 可以匹配
attribute
匹配标签上的属性。当字符串开头是属性则可以匹配
好的,看完上面四个正则, 心里有个 * 数之后,相信下面的内容你会更加清晰些
下面的内容分为
1、循环遍历 template
2、处理 头标签
3、处理 尾标签
那么我们按一个个来说
循环遍历template
通过上一篇内容,已经记录过 是怎么循环遍历 template 的了,就是通过 parseHTML 这个方法
这个方法,因为内容需要,也记录一遍
首先,什么是循环遍历template?
template 是一个字符串,所以每匹配完一个信息(比如头标签等),就会把template 截断到匹配的结束位置
比如 template 是
"<div>1111</div>"
当我们匹配完了 头标签,那么 template 就会被截断成
"1111</div>"
然后就这样一直循环匹配新的 template,直到 template 被截断成 空字符串,那么匹配完毕,其中跟截断有关的一个重要函数就是 advance
这个函数在下面的源码中用得非常多,需要牢记
其作用就是
1、截断template
2、保存当前截断的位置。比如你匹配了template到 字符串长度为5 的位置,那么 index 就是 4(从0开始)
function advance(n) {
index += n;
html = html.substring(n);
}
记住这个函数哦,我传入一个数字 n,就是要把 template 从 n 截取到结尾
然后下面就看看简化的 parseHTML 源码(如果嫌长,先跳到分析)
function parseHTML(html, options) {
// 保存所有标签的对象信息,tagName,attr,这样,在解析尾部标签的时候得到所属的层级关系以及父标签
var stack = [];
var index = 0;
var last;
while (html) {
last = html;
var textEnd = html.indexOf('<');
// 如果开头是 标签的 <
if (textEnd === 0) {
/**
* 如果开头的 < 属性尾标签
* 比如 html = '</div>'
* 匹配出 endTagMatch =["</div>", "div"]
*/
* 如果开头的 < 属性尾标签
* 比如 html = '</div>'
* 匹配出 endTagMatch =["</div>", "div"]
*/
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
// endTagMatch[0]="</div>"
advance(endTagMatch[0].length);
// endTagMatch[1]="div"
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
/**
* 如果开头的 < 属性 头标签
* parseStartTag 作用是,匹配标签存在的属性,截断 template
* html = '<div></div>'
* startTagMatch = {tagName: "div", attrs: []}
*/
* 如果开头的 < 属性 头标签
* parseStartTag 作用是,匹配标签存在的属性,截断 template
* html = '<div></div>'
* startTagMatch = {tagName: "div", attrs: []}
*/
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
}
var text ,rest ,next ;
// 模板起始位置 不是 <,而是文字
if (textEnd >= 0) {
text = html.substring(0, textEnd);
advance(textEnd);
}
// 处理文字,上篇文章已经讲过
if (options.chars && text) {
options.chars(text);
}
}
function parseStartTag(){...}
function handleStartTag(){...}
function parseEndTag(){...}
}
这段代码已经简化得很简单了,算是整体对 template 处理的一种把控我觉得
先匹配 < 的位置
1 如果 < 在template开头
那么就是标签(这里先不讨论 字符串中的 <)
然后需要多一层判断
如果是尾标签的 <,那么交给 parseEndTag 处理
如果是头标签的 <,那么使用 handleStartTag 处理
2 如果 < 不在 template 开头
那么表明 开头到 < 的这段位置是字符串,但是本文内容是标签解析,所以忽略这部分
然后每完成一次匹配,就需要调用 advace 去截断 template
然后现在,我们假定有下面这段处理
template = "<div>111</div>"
parseHTML(template)
匹配 < 在开头,正则判断之后,发现不是 尾标签的 <,那么需要判断是不是 头标签的
然后使用 parseStartTag 方法去匹配头标签信息
匹配成功,使用 handleStartTag 方法处理
看到在 parseHTML 末尾声明了三个函数,为了避免太长,我挑了出来放在相应的内容讲
而之所以会在里面声明这个三个函数,是为了在这三个函数中,能访问到 parseHTML 中的变量,比如 stack,index
处理头标签
parseStartTag
这个方法的作用就是
1、把头标签的所有信息集合起来,包括属性,标签名等
2、匹配完成之后同样调用 advance 去截断 template
3、把标签信息 返回
源码已经简化,并且有做流程注释,大家肯定看得懂,太烦的可以看后面的结果
function parseStartTag() {
// html ='<div name=1>111</div>'
// start = ["<div", "div", index: 0]
var start = html.match(startTagOpen);
if (start) {
// 存储本次头标签的信息
var match = {
tagName: start[1],
attrs: [],
start: index
};
// start[0] 是 <div
// 截断之后,template = "name=1 >111</div>"
advance(start[0].length);
var end, attr;
// 循环匹配 属性 内容,保存属性列表
// 直到 template 开头是 头标签的 >
while (
// 匹配不到头标签的 >,开始匹配 属性内容
// end = null
! (end = html.match(startTagClose))
&&
// 开始匹配 属性内容
// attr = ["name=1", "name", "=" ]
(attr = html.match(attribute))
) {
advance(attr[0].length);
match.attrs.push(attr);
}
// 匹配到 起始标签的 >,标签属性那些已经匹配完毕了
// 返回收集到的 标签信息
if (end) {
advance(end[0].length);
// 如果是单标签,那么 unarySlash 的值是 /,比如 <input />
match.unarySlash = end[1];
match.end = index;
return match
}
}
}
我们来记录下这个方法会返回什么
比如
html = "<div name=1></div>"
parseStartTag 处理之后会返回以下内容
{
tagName: "div",
attrs: [
[" name=1", "name", "=" ,
undefined, undefined, "1"]
],
unarySlash: "",
start: 0,
end: 12
}
其中 的属性
start:头标签的 < 在 template 中的位置
end:头标签的 > 在 tempalte 中的位置
attrs:是一个二维数组,存放着所有头标签的 属性信息
unarySlash:表示这个标签是否是 单标签。如果是 true,那么不是单标签。如果是 false,那么就是单标签。一切在于匹配头标签时,有没有匹配到 /
通过 parseHTML 我们看到,parseStartTag 返回的 头标签信息,给了谁呢?
没错,传给了 handleStartTag
handleStartTag
这个函数的作用
1、接收上一步收集的标签信息
2、处理属性,转换一下其格式
3、保存进 stack,记录 DOM 父子结构顺序
function handleStartTag(match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash;
// 判断是不是单标签,input,img 这些
var unary = isUnaryTag$$1(tagName) || !!unarySlash;
var l = match.attrs.length;
var attrs = new Array(l);
// 把属性数组转换成对象
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
// args = [" name=1", "name", "=",
undefined, undefined, "1" ]
var value = args[3] || args[4] || args[5] || '';
attrs[i] = {
name: args[1],
value: value
};
}
// 不是单标签,才存到 stack
if (!unary) {
stack.push({
tag: tagName,
attrs: attrs
});
}
if (options.start) {
options.start(
tagName, attrs, unary,
match.start, match.end
);
}
}
最后,把该标签得到的信息,传给 options.start,帮助建立 template 的 ast(在 上篇文章 Compile - 源码版 之 Parse 主要流程 中有说明=)
那么到这里,头标签 匹配完了
然后 template 被截断成
"111</div>"
文本处理的部分我们跳过,跳到尾标签,所以 template 为
"</div>"
然后匹配到尾标签,交给 parseEndTag 处理
那么进入我们的下一小节内容,处理尾标签
处理尾标签
在 parseHTML 中看到
当使用 endTag 这个正则成功匹配到尾标签时,会调用 parseEndTag
而 这个函数呢,可能没有那么好理解了,你可以先跳过源码,翻到后面的解析
function parseEndTag(tagName, start, end) {
var pos, lowerCasedTagName;
// 从stack 最后查找匹配的 tagName 位置
if (tagName) {
// 如果在 stack 中找不到,pos 最后是 -1
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].tagName===tagName) break
}
}
else {
// 如果没有提供标签名,那么关闭所有存在 stack 中的 起始标签
pos = 0
}
// 批量 stack pos 位置后的所有标签
if (pos >= 0) {
// 关闭 pos 位置之后所有的起始标签,避免有些标签没有尾标签
// 比如 stack.len = 7 , pos=5 ,那么就关闭 最后两个
for (var i = stack.length - 1; i >= pos; i--) {
if (options.end) {
options.end(stack[i].tag, start, end);
}
}
// 匹配完闭合标签之后,就把 匹配了的标签头 给 移除了
stack.length = pos;
}
}
函数功能分为两部分
1、找位置。从 stack 结尾,找到 tagName 所在位置 pos
2、批量闭合。闭合并移除 stack 在 pos 位置后的所有 tag
现在我们先给一个模板,然后慢慢解释
<div>
<header>
<span></span>
</header>
</div>
现在已经连续匹配到三个 头标签,div,header,span
此时 stack= [ div, header, span ]
然后开始匹配到 </span> ,然后去 stack 末尾找 span
确定 span 在 stack 的位置 pos 后,批量闭合stack 的 pos 后的所有标签
为什么从末尾开始?
因为 stack 是按 template 的标签顺序存放的,肯定是先匹配到父标签,再匹配到子标签
碰到 尾标签,肯定找最近匹配到的头标签,那么肯定是刚存入 stack 的,那么就是在 stack 的结尾
为什么闭合 pos位置后所有标签?
因为怕有刁民不写闭合标签,比如模板是这样
<div>
<header>
<span>
</header>
</div>
同样,匹配完三个头标签
stack = [ div, header, span ]
接着匹配到 </header> ,于是在 stack 末尾中找 header
在倒数第二个,那么 pos = 1
根据 stack 的长度, 遍历一次 stack,闭合到末尾,就是闭合 stack[1], stack[2]
就是闭合 header 和 span 了
就是为了避免有屁民没写 尾标签
你说单标签没有尾标签啊?
是啊,但是单标签 不存进 stack 啊哈哈哈
在 handleStartTag 中有处理哦
接下来你就可以去看 parseEndTag 的源码了,肯定能看懂
怎么闭合的呢?
parseEndTag 就做了匹配 tag 位置 和容错处理
主要实现闭合功能在调用 options.end 中,通过不断地传入尾标签从而完成闭合功能
如果你看过上篇文章 Compile - 源码版 之 Parse 主要流程 就知道,闭合是为了形成正确的节点关系树
也可以说,是为了明确节点的父子关系
总结
通过这次我们知道了
1、标签的匹配方法
2、怎么利用头标签收集信息
3、闭合标签的处理方法
最后
鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。