【Vue原理】Compile - 源碼版 之 Parse 主要流程

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

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

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

【Vue原理】Compile - 源碼版 之 Parse 主要流程 數組

本文難度較繁瑣,須要耐心觀看,若是你對 compile 源碼暫時不感興趣能夠先移步白話版 Compile - 白話版,緩存

parse 是 渲染三巨頭的老大,其做用是把 template 字符串模板,轉換成 astbash

其涉及源碼也是多得一批,達到了 一千多行,想一想若是我把所有源碼放到文章裏面來簡直不能看,因此我打算只保留主要部分,就是正常流程能夠走通,去掉那些特殊處理的地方函數

大部分源碼都是特殊處理,好比 script ,style,input ,pre 等標籤,此次所有都去掉,只留下通用元素的處理流程,留下一個骨架學習

由於 parse 的內容很是的多,除了精簡源碼以外,我還經過不一樣內容劃分文章去記錄優化

今天,要記錄的就是 parse 解析 template 成 ast 的大體流程,而怎麼解析標籤名,怎麼解析標籤屬性會暫時忽略,而獨立成文。當有解析標籤名和解析屬性的地方會直接出結果。好比當我說在 模板 "<div></div>" 匹配出頭標籤時,直接就獲得 div ,而不會去考究是如何匹配出來的ui

好的,到底 template 是怎麼變成 ast 的呢?跟着我去探索把~spa


AST

先來講說 ast 吧,這種複雜的概念,反正是須要查的。因此本文根本不須要解釋太多

直接說個人理解吧

抽象語法樹,以樹狀形式表現出語法結構

直接使用例子去直觀感覺就行了

<div>111</div>
複製代碼

用 ast 去描述這個模板就是

{ 
    tag:'div',    

    type :1 , 

    children:[ { 
        type:3, 
        text:'11' 
    } ] 
}
複製代碼

簡單得一批把,複雜的這裏也不提了,反正跟 parse 沒多大關係我以爲

另外記一下,節點的 type 表示的意思

type:1,節點

type:2,表達式,好比 {{isShow}}

type:3,純文本

如今就開始 parse 的內容了,那麼就看 parse 的源碼


Parse

parse 是渲染三巨頭的老大,同時它也是一個函數,源碼以下

function parse(template) {    



    var stack = []; // 緩存模板中解析的每一個節點的 ast

    var root;   // 根節點,是 ast
    var currentParent; // 當前解析的標籤的父節點

    /**
    * parseHTML 處理 template 匹配標籤,再傳入 start,end,chars 等方法
    **/
    parseHTML(template, {        

        start: (..被抽出,在後面)

         end: (..被抽出,在後面), // 爲 起始標籤 開啓閉合節點
         chars: (..被抽出,在後面) // 文字節點
    });    



    return root

}
複製代碼

parse 接收 template 字符串,使用 parseHTML 這個函數在 template 中匹配標籤

並傳入 start,end,chars 三個函數 供 parseHTML 處理標籤等內容

start,end,chars 方法都已經被我抽出來,放在後面逐個說明

下面來看下其中聲明的三個變量

1 stack

是一個數組存放模板中按順序 從頭至尾 每一個標籤的 ast

注:不會存放單標籤的 ast ,好比 input,img 這些

好比 stack 是這樣的

stack=[{ 
    tag:'div',    

    type :1 , 

    children:[ { 
        type:3, 
        text:'11' 
    } ] 
}]
複製代碼

主要做用是幫助理清節點父子關係

2 root

每一個模板都必須有一個根節點。寫過 Vue 項目的都知道了,因此通常解析到第一個標籤的時候,會直接設置這個標籤爲 根節點

而且最後返回的也是 root

不能夠存在兩個根節點(有 v-if 的不討論)

3 currentParent

在解析標籤的時候,必需要知道這個標籤的 父節點時誰

這樣才知道 這個標籤是誰的子節點,才能把這個節點添加給相應的 節點的 children

注:根節點 沒有 父節點,因此就是 undefined

parse 源碼已經被我精簡得很簡單了,主要內容其實就在 其中涉及的四個方法中

parseHTML,start,end,chars

parseHTML 是處理 template 的主力,其餘三個函數是功能類型的,負責處理相應的內容。 例如,start 是處理頭標籤的,end 是處理尾標籤的,chars 是處理文本的

先來看看 parseHTML


處理 template

parseHTML 做爲處理 template,匹配標籤的函數,是十分龐大的,其中兼顧了很是多狀況的處理

而本次在不影響流程的狀況下,我去掉了下面這些處理,優化閱讀

一、沒有結束標籤的處理

二、文字中包含 < 的處理

三、註釋的處理

四、忽略首尾空白字符,默認起始和結尾都是標籤

我的認爲主要內容爲三個

一、循環 template 匹配標籤

二、把匹配到的內容,傳給相應的方法處理

三、截斷 template

來看源碼,已經簡化得不行了,可是仍是要花點心思看看

function parseHTML(html, options) {    



    while (html) {       



         // 尋找 < 的起始位置

        var textEnd = html.indexOf('<'),
            text ,rest ,next;        



        // 模板起始位置是標籤開頭 <

        if (textEnd === 0) {   

               

            /**
             * 若是是尾標籤的 <
             * 好比 html = '</div>' , 匹配出 endTagMatch =["</div>", "div"]
             */
            var endTagMatch = html.match(endTag);            



            if (endTagMatch) {      

          

                // endTagMatch[0]="</a>"

                html = html.substring(endTagMatch[0].length); 

              

                // 處理尾標籤,方法後面有記錄
                options.end();                



                continue

            }   

                

            /**
             * 若是是起始標籤的 <
             * parseStartTag 做用是,匹配標籤存在的屬性,截斷 template
             * html = '<div></div>', 

             * parseStartTag 處理以後,startTagMatch = {tagName: "div", attrs: []}

             */
            var startTagMatch = parseStartTag();   

       

            // 匹配到 起始標籤以後
            if (startTagMatch) {  

             

                // 處理起始標籤,後面有介紹
                options.start(起始標籤的信息);                



                continue

            }
        }        



        // 模板起始位置不是 <,而是文字

        if (textEnd >= 0) {
            text = html.substring(0, textEnd);
            html = html.substring(n);
        }       



        // 處理文字,後面有介紹
        if (options.chars && text) {
            options.chars(text);
        }
    }
}
複製代碼

思路以下

1匹配 < 這個符號

由於他是標籤的開頭(已經排除了文字中含有 < 的處理,不作討論)

2若是 template 開頭是 <

那麼多是 尾標籤,多是 頭標籤,那麼就須要判斷究竟是哪一個

一、先匹配尾標籤,若是匹配到,那麼就是尾標籤,使用 end 方法處理。

二、若是不是,使用 parseStartTag 函數匹配獲得首標籤,並把 首標籤信息傳給 start 處理

parseStartTag 就是使用正則在template 中匹配出 首標籤信息,其中包括標籤名,屬性等

好比 template 是

html = '<div name="22">111</div>;'
複製代碼

parseStartTag 處理匹配以後獲得

{    

    tagName: "div", 

    attrs: [{name:"22"}]
}
複製代碼

3 若是 template 開頭不是 <

那麼證實 開頭 到 < 的位置這一段,是字符串,那麼就是文本了

傳給 chars 方法處理

每次處理一次,就會截斷到匹配的位置,而後 template 愈來愈短,直接爲空,退出 while,因而處理完畢

對於截斷呢,使用 substring,可能忘了怎麼做用的,寫個小例子

傳入數字,表示這個位置前面的字符串都不要

image

而後,就到了咱們其餘三個方法的閃亮登場了


處理頭標籤

每當 parseHTML 匹配到一個 首標籤,都會把該標籤的信息傳給 start 方法,讓他來處理

function start(tag, attrs, unary) {    



    // 建立 AST 節點

    var element = createASTElement(tag, attrs, currentParent);      



    /**
     * ...省略了一段處理 vFor,vIf,解析 @ 等屬性指令的代碼
     **/

    // 設置根節點,一個模板只有一個根節點
    if (!root) root = element;    



    // 處理父子關係

    if (currentParent) {
        currentParent.children.push(element);
        element.parent = currentParent;
    }    



    // 不是單標籤(input,img 那些),就須要保存 stack

    if (!unary) {
        currentParent = element;
        stack.push(element);
    }
}
複製代碼

精簡得一目瞭然(面目全非),看得極度溫馨

看看 start 方法都作了哪些惡呢

一、建立 ast

二、解析 attrs,並存放到 ast (已省略屬性解析)

三、設置根節點,父節點,把節點添加進父節點的 children

四、ast 保存進 stack

好像不用解釋太多,確定都看得懂啊,除了一個 建立 ast 的函數

這就來源碼

function createASTElement(tag, attrs, parent) {    



    return {        

        type: 1,        

        tag: tag,        

        attrsList: attrs,        

        // 把 attrs 數組 轉成 對象

        attrsMap: makeAttrsMap(attrs),        

        parent: parent,        

        children: []

    }
}
複製代碼

建立一個 ast 結構,保存數據

直接返回一個對象,很是明瞭,包含的各類屬性,應該也能看懂

其中有一個 makeAttrsMap 函數,舉個栗子

模板上的屬性,通過 parseHTML 解析成一個數組,以下

[{    

    name:"hoho" ,value:"333"

},{    

    name:"href" ,value:"444"

}]
複製代碼

makeAttrMap 轉成對象成這樣

{ hoho:"333",   href:"444"}
複製代碼

而後就保存在 ast 中


處理尾標籤

每當 parseHTML 匹配到 尾標籤 ,好比 "" 的時候,就會調用傳入的 end 方法

來看看吧

function end() {    

    // 標籤解析結束,移除該標籤

    stack.length -= 1;
    currentParent = stack[stack.length - 1];
}
複製代碼

乍一看,很簡單啊!這麼少(都是精簡...)

做用有兩個

1從 stack 數組中移除這個節點

stack 保存的是匹配到的頭標籤,若是標籤已經匹配結束了,那麼就須要移除

stack 就是爲了明確各節點間父子關係而存在的

保證 stack 中最後一個節點,永遠是下次匹配的節點的父節點

舉個栗子,存在下面模板

公衆號

stack 匹配兩個 頭標籤以後

stack = [ 'div' , 'section']
複製代碼

看看 start 能夠知道,此時 currentParent = section

而後匹配到 ,則移除 stack 中的 section,而且重設 currentParent

stack = ['div']

currentParent = 'div'
複製代碼

再匹配到 p 的時候,p 的父節點就是 div,父子順序就是正確的了

2從新設置 stack 最後一個節點爲父節點


處理文本字符串

當 parseHTML 去匹配 < 的時候,發現 template 不是 <,template開頭 到 < 還有一段距離

那麼這段距離的內容就是 文本了,那麼就會把這段文本傳給 chars 方法處理

來看看源碼

function chars(text) {    



    // 必須存在根節點,不可能用文字開頭

    if(!currentParent) return



    var children = currentParent.children;    



    // 經過 parseText 解析成字符串,判斷是否含有雙括號表達式,好比 {{item}}

    // 若是是有表達式,會存放多一些信息,
    var res = parseText(text)    



    if(res) {

        children.push({            

            type: 2,            

            expression: res.expression,            

            tokens: res.tokens,            

            text: text

        });
    }    



    // 普通字符串,直接存爲 字符串子節點

    else if(
      !children.length ||
      children[children.length - 1].text !== ' '
    ) {
        children.push({            

            type: 3,            

            text: text

        });
    }
}
複製代碼

這段代碼主要做用就是,爲 父節點 添加 文本子節點

而文本子節點分爲兩種類型

一、普通型,直接存爲文本子節點

二、表達式型,須要通過 parseText 處理

直接以結果來定義吧

好比處理這段文本

{{isShow}}

{    

    expression: toString(isShow)

    tokens: [{@binding: "isShow"}]
}
複製代碼

主要是爲了把表達式 isShow 拿到,方便後面從實例上獲取值

好的,如今,template 處理流程所涉及的主要方法都講完了

如今用上面這些函數來走一個流程

如今有一個模板

<div>11 </div>
複製代碼

1 開始循環 tempalte

匹配到第一個 頭標籤 (

),傳入 parse-start,生成 對應的 ast

該 div 的 ast 變成根節點 root,並設置其爲當前父節點 currentParent,保存進節點緩存數組 stack

此時

stack = [ { tag:'div' , children:[ ] } ]
複製代碼

第一輪處理結束,template 截斷到第一次匹配到的位置

此時,template = 11

2 開始第二次遍歷

開始匹配 <,發現 < 不在開頭,而 開頭位置 到 < 有一段普通字符串

調用 parse-char,傳入字符串

發現其沒有 雙括號表達式,直接給父節點添加簡單子節點

currentParent.children.push({ type:3 , text:'11' })
複製代碼

此時

stack =[ { tag:'div' , children:[ { type:3 , text:'11' } ] } ]
複製代碼

第二輪處理結束,template 截斷到剛剛匹配完的字符串

此時,template =

3 開始第三輪遍歷

繼續尋找 <,發現就在開頭,可是這是一個結束標籤,標籤名是 div

由於 stack 是節點順序存入的,這個結束標籤確定屬於 stack 最後一個 標籤

因爲 該標籤匹配完畢,因此從 stack 中移除

而且設置 當前父節點 currentParent 爲 stack 倒數第二個

第三次遍歷結束,template 繼續截斷

此時 template 爲空了,結束全部遍歷

返回這次 tempalte 解析的 root

{ 
    tag:'div',type :1 , 
    children:[ { type:3 , text:'11' } ] 
}
複製代碼

因而 parse 就成功把 tempalte 解析成了 ast ,就是 root


總結

本問講的是 parse 的主要流程,忽略了內部的處理細節,好比怎麼解析標籤,怎麼解析屬性,其餘內容都會獨立成文章

在 parse 的流程中,大體有五個函數,咱們屢一下,以下

parse,parseHTML,start,end,chars

parse 是整個 parse 流程的總函數

parseHTML 是 parse 處理的主力函數

start,end,chars 是 在 parse 中傳給 parseHTML ,用來幫助處理 匹配的標籤信息的函數,這三個函數會在 parseHTML 中被調用


最後

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

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