【Vue原理】Compile - 源码版 之 Parse 标签解析

1,149 阅读10分钟

写文章不容易,点个赞呗兄弟

专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本 【2.5.17】

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧

【Vue原理】Compile - 源码版 之 标签解析

咳咳,上一篇文章,我们已经大致把 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 ]

然后开始匹配到 ,然后去 stack 末尾找 span

确定 span 在 stack 的位置 pos 后,批量闭合stack 的 pos 后的所有标签

为什么从末尾开始?

因为 stack 是按 template 的标签顺序存放的,肯定是先匹配到父标签,再匹配到子标签

碰到 尾标签,肯定找最近匹配到的头标签,那么肯定是刚存入 stack 的,那么就是在 stack 的结尾

为什么闭合 pos位置后所有标签?

因为怕有刁民不写闭合标签,比如模板是这样

<div>
    <header>
        <span>
    </header>

</div>

同样,匹配完三个头标签

stack = [ div, header, span ]

接着匹配到 ,于是在 stack 末尾中找 header

在倒数第二个,那么 pos = 1

根据 stack 的长度, 遍历一次 stack,闭合到末尾,就是闭合 stack[1], stack[2]

就是闭合 header 和 span 了

就是为了避免有屁民没写 尾标签

你说单标签没有尾标签啊?

是啊,但是单标签 不存进 stack 啊哈哈哈

在 handleStartTag 中有处理哦

接下来你就可以去看 parseEndTag 的源码了,肯定能看懂

怎么闭合的呢?

parseEndTag 就做了匹配 tag 位置 和容错处理

主要实现闭合功能在调用 options.end 中,通过不断地传入尾标签从而完成闭合功能

如果你看过上篇文章 Compile - 源码版 之 Parse 主要流程 就知道,闭合是为了形成正确的节点关系树

也可以说,是为了明确节点的父子关系


总结

通过这次我们知道了

1、标签的匹配方法

2、怎么利用头标签收集信息

3、闭合标签的处理方法


最后

鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢

公众号