【Vue原理】Compile - 源碼版 之 Parse 標籤解析

寫文章不容易,點個讚唄兄弟

專一 Vue 源碼分享,文章分爲白話版和 源碼版,白話版助於理解工做原理,源碼版助於瞭解內部詳情,讓咱們一塊兒學習吧 研究基於 Vue版本 【2.5.17】html

若是你以爲排版難看,請點擊 下面連接 或者 拉到 下面關注公衆號也能夠吧數組

【Vue原理】Compile - 源碼版 之 標籤解析 bash

咳咳,上一篇文章,咱們已經大體把 parse 的流程給記錄了一遍,若是沒看過,比較建議,先把這個流程給看了 Compile - 源碼版 之 Parse 主要流程 函數

可是忽略了其中的處理細節,好比標籤怎麼解析的,屬性怎麼解析的,並且這兩個內容也是很是多的,因此須要單獨拎出來詳細記錄,否則混在一塊兒,又臭又長學習

白話版在這~ Compile - 白話版 ui

今天的內容是,記錄 標籤解析的 源碼spa

首先,開篇以前呢,咱們來了解一下文章會出現過的正則3d


相關正則

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"'=<>`]+)))?/;
複製代碼

主要是四個rest

startTagOpen

匹配 頭標籤的 前半部分。當字符串開頭是 頭標籤時,能夠匹配code

公衆號

startTagClose

匹配 頭標籤的 右尖括號。當字符串開頭是 > 時,能夠匹配

公衆號

endTag

匹配 尾標籤。當字符串開頭是 尾標籤時 能夠匹配

公衆號

attribute

匹配標籤上的屬性。當字符串開頭是屬性則能夠匹配

公衆號

好的,看完上面四個正則, 內心有個 * 數以後,相信下面的內容你會更加清晰些

下面的內容分爲

一、循環遍歷 template

二、處理 頭標籤

三、處理 尾標籤

那麼咱們按一個個來講


循環遍歷template

經過上一篇內容,已經記錄過 是怎麼循環遍歷 template 的了,就是經過 parseHTML 這個方法

這個方法,由於內容須要,也記錄一遍

首先,什麼是循環遍歷template?

template 是一個字符串,因此每匹配完一個信息(好比頭標籤等),就會把template 截斷到匹配的結束位置

好比 template 是

"<div>1111</div>"
複製代碼

當咱們匹配完了 頭標籤,那麼 template 就會被截斷成

"1111</div>"
複製代碼

而後就這樣一直循環匹配新的 template,直到 template 被截斷成 空字符串,那麼匹配完畢,其中跟截斷有關的一個重要函數就是 advance

這個函數在下面的源碼中用得很是多,須要牢記

其做用就是

一、截斷template

二、保存當前截斷的位置。好比你匹配了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

這個方法的做用就是

一、把頭標籤的全部信息集合起來,包括屬性,標籤名等

二、匹配完成以後一樣調用 advance 去截斷 template

三、把標籤信息 返回

源碼已經簡化,而且有作流程註釋,你們確定看得懂,太煩的能夠看後面的結果

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

這個函數的做用

一、接收上一步收集的標籤信息

二、處理屬性,轉換一下其格式

三、保存進 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;
    }
}
複製代碼

函數功能分爲兩部分

一、找位置。從 stack 結尾,找到 tagName 所在位置 pos

二、批量閉合。閉合並移除 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 主要流程 就知道,閉合是爲了造成正確的節點關係樹

也能夠說,是爲了明確節點的父子關係


總結

經過此次咱們知道了

一、標籤的匹配方法

二、怎麼利用頭標籤收集信息

三、閉合標籤的處理方法


最後

鑑於本人能力有限,不免會有疏漏錯誤的地方,請你們多多包涵,若是有任何描述不當的地方,歡迎後臺聯繫本人,有重謝

公衆號
相關文章
相關標籤/搜索