【Vue原理】Compile - 源碼版 之 Parse 屬性解析

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

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

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

【Vue原理】Compile - 源碼版 之 屬性解析 dom

哈哈哈,今天終於到了屬性解析的部分了,以前已經講過了 parse 流程,標籤解析,最後就只剩下 屬性解析了 (´・ᴗ・`)ide

若是你對 compile 不感興趣的就先不看把,畢竟不會立刻起到什麼做用~~ヾ(●´∀`●)函數

公衆號

若是大家沒看過前面兩篇文章的,十分建議看一下~學習

Compile 之 Parse 主要流程 Compile 之 標籤解析 ui

若是看了,大家應該知道《屬性解析》在哪部分中,沒錯,在處理 頭標籤的 部分 parse-start 中this

那麼咱們就來到 parse - start 這個函數中!spa

看到下面的源碼中,帶有 process 的函數都是用於處理 屬性的3d

function parse(template){
    
    parseHTML(template,{        

        start:(...抽出放下面)

    })
}



function start(tag, attrs, unary) {    



    // 建立 AST 節點

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



    // 節點須要解析,並無尚未處理

    if (!element.processed) {

        processFor(element);
        processIf(element);
        processSlot(element);        



        for (var i = 0; i < transforms.length; i++) {

            element = transforms[i](element, options) || element;
        }

        processAttrs(element);
    }
    
    .... 省略部分不重要代碼  



    // 父節點就是上一個節點,直接放入 上一個節點的 children 數組中

    if (currentParent) {      



        // 說明前面節點有 v-if

        if (element.elseif || element.else) {
            processIfConditions(element, currentParent);

        } else {
            currentParent.children.push(element);
            element.parent = currentParent;
        }
    }
}
複製代碼

看完了吧,上面處理屬性的函數大概有幾個

沒啥難的,就是內容多了點

一、processFor,解析 v-for

二、processIf,解析 v-if

三、processSlot,解析 slot

四、processAttrs,解析其餘屬性

五、transforms,解析樣式屬性
複製代碼

而且只有 element.processed 爲 false 的時候,纔會進行解析

由於 element.processed 表示屬性已經解析完畢,一開始 element.processed 的值是 undefined

下面就會逐個說明上面的方法

先明確下 element 是什麼?

parse 流程中說過了,element 是 經過解析獲得的 tag 信息,生成的 ast

下面會逐個分析下上面的四個函數,並會附上相應的 element 例子做爲參考

其實還有不少其餘處理函數,爲了維持文章的長度,因此我去掉了

開篇以前,你們須要先了解 getAndRemoveAttr 這個函數,下面不少地方都會使用到

做用就是從 el.attrList 中查找某個屬性,返回返回屬性值

function getAndRemoveAttr(el, name, removeFromMap) {  



    var val =el.attrsMap[name];  



    if (removeFromMap) {        

        delete el.attrsMap[name];

    }    



    return val

}
複製代碼

parse-start 中的 tramsforms

在parse -start 這個函數的 開頭,咱們看到有一個 transfroms 的東西

transforms 是一個數組,存放兩個函數,一個是處理 動靜態的 class,一個處理 動靜態的 style

兩種處理都很簡單的,咱們來簡單看看處理結果就行了

處理 class

function transformNode(el, options) {    



    var staticClass = getAndRemoveAttr(el, 'class');    



    if (staticClass) {

        el.staticClass = JSON.stringify(staticClass);
    }    



    // :class="b" 直接返回 b
    var classBinding = getBindingAttr(el, 'class', false);    



    if (classBinding) {

        el.classBinding = classBinding;
    }
}
複製代碼

公衆號

{    

    classBinding: "b"

    staticClass: ""a""
    tag: "span"
    type: 1

}
複製代碼

處理 style

function transformNode$1(el, options) {    



    var staticStyle = getAndRemoveAttr(el, 'style');    



    if (staticStyle) {        

        // 好比綁定 style="height:0;width:0"

        // parseStyleText 解析獲得對象 { height:0,width:0 }
        el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
    }  



    // :style="{height:a}" 解析得 {height:a}
    var styleBinding = getBindingAttr(el, 'style', false);    

    if (styleBinding) {

        el.styleBinding = styleBinding;
    }
}
複製代碼

公衆號

{    

    staticStyle: "{"width":"0"}"

    styleBinding: "{height:a}"
    tag: "span"
    type: 1

}
複製代碼

解析 v-for

在 parse - start 這個函數中,看到了 processFor,沒錯,就是解析 v-for 指令的!

function processFor(el) {    



    var exp = getAndRemoveAttr(el, 'v-for')    



    if (exp) {        

        // 好比指令是 v-for="(item,index) in arr"

        // res = {for: "arr", alias: "item", iterator1: "index"}
        var res = parseFor(exp);        

        if (res) {            

            // 把 res 和 el 屬性合併起來

            extend(el, res);
        } 
    }
}
複製代碼

沒有什麼難度,直接看模板 和最終結果好了

<div v-for="(item,index) in arr"></div>
複製代碼
{    

    alias: "item",    

    for: "arr",    

    iterator1: "index",    

    tag: "div",    

    type: 1,

}
複製代碼

解析 v-if

在 parse - start 這個函數中,看到了 processFor,沒錯,就是解析 v-if 指令的!

function processIf(el) {    



    var exp = getAndRemoveAttr(el, 'v-if');    



    if (exp) {


        el.if = exp;
        
        (el.ifConditions || el.ifConditions=[])
        .push({            

            exp: exp,            

            block: el

        })
    } else {        



        if (getAndRemoveAttr(el, 'v-else') != null) {

            el.else = true;
        }        



        var elseif = getAndRemoveAttr(el, 'v-else-if');        



        if (elseif) {

            el.elseif = elseif;
        }
    }
}
複製代碼

處理 v-if 上是這樣的,須要把 v-if 的 表達式 和 節點都保存起來

而 v-else ,只須要設置 el.else 爲 true,v-else-if 一樣須要保存 表達式

在這裏 v-else 和 v-else-if 並無作太多處理,而是在最前面的 parse-start 中有處理

if (element.elseif || element.else) {
    processIfConditions(element, currentParent);
} 
複製代碼

當通過 processIf 以後,該屬性存在 elseif 或 else

那麼會調用一個方法,以下

function processIfConditions(el, parent) {    



    var prev = findPrevElement(parent.children);    



    if (prev && prev.if) {    

    
        (prev.ifConditions ||prev.ifConditions=[])
        .push({            
            exp: el.elseif,            

            block: el

        })
    }
} 
複製代碼

這個方法主要是把 帶有 v-else-if 和 v-else 的節點掛靠在 帶有 v-if 的節點上

先來看掛靠後的結果

<div>
    <p></p>
    <div v-if="a"></div>
    <strong v-else-if="b"></strong>
    <span v-else></span>

</div>
複製代碼
{    

    tag: "header",    

    type: 1,    

    children:[{        

        tag: "header",        

        type: 1,        

        if: "a",        

        ifCondition:[

            {exp: "a", block: {header的ast 節點}}
            {exp: "b", block: {strong的ast 節點}}
            {exp: undefined, block: {span的ast節點}}
        ]
    },{        

        tag: "p"

        type: 1
    }]
} 
複製代碼

咱們能夠看到,原來寫的兩個子節點,strong 和 span 都不在 div 的children 中

而是跑到了 header 的 ifCondition 裏面

如今看看 processIfConditions , 這個方法是隻會處理 帶有 v-else-if 和 v-else 的節點的

而且須要找到 v-if 的節點掛靠,怎麼找的呢?你能夠看到一個方法

function findPrevElement(children) {    



    var i = children.length;    



    while (i--) {        

        if (children[i].type === 1) {            

            return children[i]

        } else {
            children.pop();
        }
    }
} 
複製代碼

從同級子節點中結尾開始找,當type ==1 的時候,這個節點就是帶有 v-if 的節點

那麼 v-else 那兩個就能夠直接掛靠在上面了

你會問,爲何從結尾不是返回 span 節點,爲何 type ==1 就是帶有 v-if?

首先,你並不能從正常解析完的角度去分析,要從標籤逐個解析的角度去分析

好比如今已經解析完了 v-if 的節點,而且添加進了 父節點的 children

而後解析下一個節點,好比這個節點是帶有 v-else-if 的節點,此時,再去 parent.children 找最後一個節點(也就是剛剛添加進去的 v-if 節點)

確定返回的是 v-if 的節點,天然能正確掛靠了

v-else 同理

若是你說 v-if 和 v-else-if 隔了一個其餘節點,那 v-else-if 就沒法掛靠在 v-if 了呢

那你確定是刁民,v-else-if 必須跟着 v-if 的,不然都會報錯,錯誤就不討論了


解析 slot

在 parse - start 這個函數中,看到了 processSlot,沒錯,就是解析 slot 相關

function processSlot(el) {    



    if (el.tag === 'slot') {

        el.slotName = el.attrsMap.name



    } else {        

        

        var slotScope = getAndRemoveAttr(el, 'slot-scope')

        el.slotScope = slotScope;     

   

        // slot 的名字
        var slotTarget = el.attrsMap.slot        



        if (slotTarget) {

            el.slotTarget = 
                slotTarget === '""' 

                ? '"default"'

                : slotTarget;

        }
    }
}
複製代碼

這個好像也沒什麼好講的,就簡單記錄一下 解析的結果好了

子組件模板

<span>
    <slot name=" header"
        :a="num" :b="num">
    </slot>
</span>
複製代碼

解析成

{  
    tag: "span"
    type: 1
    children:[{  
        attrsMap: {name: " header", :a: "num", :b: "num"}
        slotName: "" header""
        tag: "slot"
        type: 1
    }]
}
複製代碼

父組件模板

<div>
    <child >
        <p slot="header" slot-scope="c"> {{ c }}</p>
    </child>

</div>
複製代碼

解析成

{    

    children: [{        

        tag: "child",        

        type: 1,        

        children: [{            

            slotScope: "c",            

            slotTarget: ""header "",            

            tag: "p",            

            type: 1

        }]
    }],    

    tag: "div",    

    type: 1

}
複製代碼

下面內容不少,可是不難


解析其餘屬性

這一塊內容不少,可是總的來講沒有難度,就是看得煩了一些,而後把源碼放到了最後,打算先寫解析

這裏集中處理了剩下的其餘類型的屬性,大體分了兩種狀況

1Vue 自帶屬性

好比 帶有 "v-" , ":" , " @" 三種符號的屬性名,這三種每種都會分開處理

而在這三種屬性開始處理前,會把屬性名帶有的 modifiers 給提取出來

好比帶有 modifiers 的指令

v-bind.a.b.c = "xxxx"
複製代碼

通過處理,會提取出 modifiers 對象,以下

{a: true, b: true, c: true}
複製代碼

以供指令使用

以後就開始處理三種類型屬性

1 " : "

咱們都知道 " : " 等於 "v-bind" ,全部當匹配到這種屬性名的時候,會進入這裏的處理

大體看一遍以後,能夠看到,通過這部分的處理

屬性會存放進 el.props 或者 el.attrs

那麼問題來了?

怎麼判斷屬性放入 el.props 仍是 el.attrs 呢?

有兩種條件

一、modifiers.prop

當你給指令添加了 .prop 的時候,好比

<div :sex.prop="myName"></div>
複製代碼

那麼 sex 這個屬性,就會被存放到 el.props

二、表單

你看到這一句代碼

!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
複製代碼

第一,不能是組件

第二,是表單元素,且是表單重要屬性

來看看 platformMustUseProp 吧,很容易

當元素是 input,textarea,option,select,progress

屬性是 selected ,checked ,value 等之類的話

都要存放到 el.props 中

function a(tag, type, attr) {    



    return (

        (attr === 'value' && 'input,textarea,option,select,progress'.indexOf(tag)>-1) 
        && type !== 'button' 
        || (attr === 'selected' && tag === 'option') 
        || (attr === 'checked' && tag === 'input') 
        || (attr === 'muted' && tag === 'video')
    )
};
複製代碼

或許你會問

el.props 和 el.attrs 有什麼區別呢?

props 是直接添加到 dom 屬性上的,而不會顯示在標籤上

公衆號
公衆號

attrs 則是用於顯示到到 標籤屬性上的

公衆號
公衆號

還有一個問題

添加進 el.props 的屬性,爲何要轉換成駝峯命名?

你看到的,全部屬性名,都會經過一個 camelize 的方法,爲何呢?

由於 DOM 的屬性都是駝峯命名的,不存在橫杆的命名

因此要把 a-b 的命名都轉成 aB,隨便截了一張圖

公衆號

然而 innerHTML 比較特殊,駝峯都不行,因此作了特殊處理,你也看到的

駝峯的方法應該挺有用的,放上來吧

var camelize = function(str) {    



    return str.replace(/-(\w)/g, function(_, c) { 

        return c ? c.toUpperCase() : ''; 
    })
})
複製代碼

modifiers.sync

以後,你應該還發現了一塊寶藏,沒錯就是 sync

相信你應該用過吧,用於父子通訊的,子組件想修改父組件傳入的 prop

經過事件的方式,間接修改 父組件的數據,從而更新 props

爲了不你們不記得了,在這裏貼一個使用例子

父組件 給 子組件 傳入 name ,加入 sync 能夠雙向修改

<div>    

    <child-test :name.sync="xxx"></child-test>

</div>



子組件想修改 父組件傳入的 name,直接觸發事件並傳入參數就能夠了

this.$emit("update:name", 222)
複製代碼

因而如今咱們來看他在屬性解析時是怎麼實現的

addHandler(el, 
    "update:" + camelize(name),
    genAssignmentCode(value, "$event")
);
複製代碼

看看這段代碼作了什麼

首先

camelize(name)
複製代碼

把名字變成駝峯寫法,好比 get-name,轉換成 getName

而後下面這段代碼 執行

genAssignmentCode(value, "$event")
複製代碼

解析返回 "value = $event"

而後 addHandler 就是把 事件名和事件回調保存到 el.events 中,以下

公衆號

保存的 events 後面會被繼續解析,value 會被包一層 function

至關於給子組件監聽事件

@update:name ="function($event){ xxx = $event }"
複製代碼

$event 就是子組件觸發事件時 傳入的值

xxx 是 父組件的數據,賦值以後,就至關於子組件修改父組件數據了

要是想了解 event 的內部原理,能夠看 Event - 源碼版 之 綁定組件自定義事件

2 " @ "

當匹配到 @ 或者 v-on 的時候,屬於添加事件,這裏沒有太多處理

addHandler 就是把全部事件保存到 el.events

公衆號

公衆號

3 " v- "

剩下 帶有 v- 的屬性,都會放到這裏處理

匹配參數的,源碼中註釋也說清楚了,這裏不解釋了

而後通通保存到 el.directives 中

公衆號

公衆號

2普通屬性

沒啥說的,普通屬性,直接存放進 el.attrs

公衆號

公衆號

下面就是處理其餘屬性的源碼,你別看很長,其實很簡單的!

公衆號

var onRE = /^@|^v-on:/;

var dirRE = /^v-|^@|^:/;

var bindRE = /^:|^v-bind:/;

var modifierRE = /\.[^.]+/g;

var argRE = /:(.*)$/;



function processAttrs(el) {    



    var list = el.attrsList;    

    var i, l, name, rawName, value, modifiers, isProp;   



    for (i = 0, l = list.length; i < l; i++) {


        name = rawName = list[i].name;
        value = list[i].value;        



        // 判斷屬性是否帶有 'v-' , '@' , ':'

        if (dirRE.test(name)) {            



            // mark element as dynamic

            el.hasBindings = true;       

     

            // 好比 v-bind.a.b.c = "xxzxxxx"

            // 那麼 modifiers = {a: true, b: true, c: true}
            modifiers = parseModifiers(name);            



            // 抽取出純名字

            if (modifiers) {    

            

                // name = "v-bind.a.b.c = "xxzxxxx" "

                // 那麼 name= v-bind
                name = name.replace(modifierRE, '');
            }        

    

            // 收集動態屬性,v-bind,多是綁定的屬性,多是傳入子組件的props

            // bindRE = /^:|^v-bind:/
            if (bindRE.test(name)) {   

            

                // 抽取出純名字,好比 name= v-bind

                // 替換以後,name = bind
                name = name.replace(bindRE, '');
                isProp = false;      

          

                if (modifiers) {   

                 

                    // 直接添加到 dom 的屬性上
                    if (modifiers.prop) {
                        isProp = true;    

                    

                        // 變成駝峯命名

                        name = camelize(name);                        

                        if (name === 'innerHtml')   

                            name = 'innerHTML'; 

                    }      

              

                    // 子組件同步修改
                    if (modifiers.sync) {
                        addHandler(el,      

                            // 獲得駝峯命名                      

                            "update:" + camelize(name), 

                            // 獲得 "value= $event"

                            genAssignmentCode(value, "$event")
                        );
                    }
                }  

              

                // el.props 的做用上面有說,這裏有部分是 表單的必要屬性都要保存在 el.props 中
                if (

                     isProp ||

                     // platformMustUseProp 判斷這個屬性是否是要放在 el.props 中

                     // 好比表單元素 input 等,屬性是value selected ,checked 等

                     // 好比 tag=input,name=value,那麼value 屬性要房子啊 el.props 中

                     (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))

                ) {


                    (el.props || (el.props = [])).push({ 
                        name, 
                        value
                    });
                } 

                // 其餘屬性放在 el.attrs 中

                else {


                    (el.attrs || (el.attrs = [])).push({ 
                        name, 
                        value
                    });
                }
            }            



            // 收集事件,v-on , onRE = /^@|^v-on:/

            else if (onRE.test(name)) {    

            

                // 把 v-on 或者 @ 去掉,拿到真正的 指令名字
                // 好比 name ="@click" , 替換後 name = "click"
                name = name.replace(onRE, '');
                addHandler(el, name, value, modifiers, false);
            }            



            // 收集其餘指令,好比 "v-once"else { 

                // 把v- 去掉,拿到真正的 指令名字
                name = name.replace(dirRE, '');                



                // name = "bind:key" , argMatch = [":a", "a"]

                var argMatch = name.match(argRE);                

                var arg = argMatch && argMatch[1];    

            

                if (arg) {                    

                    // 好比 name = "bind:key" ,去掉 :key

                    // 而後 name = "bind"
                    name = name.slice(0, -(arg.length + 1));
                }

                (el.directives || (el.directives = [])).push({ 
                    name, 
                    rawName, 
                    value, 
                    arg, 
                    modifiers
                });
            }

        } else {

            (el.attrs || (el.attrs = [])).push({ 
                name, 
                value
            });
        }
    }
}
複製代碼

最後

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

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