【Vue原理】Compile - 源碼版 之 generate 節點拼接

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

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

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

【Vue原理】Compile - 源碼版 之 generate 節點拼接 express

終於走到了 Vue 渲染三巨頭的最後一步了,那就是 generate,反正文章已經寫完了,乾脆早點發完,反正這部份內容你們也不會立刻看哈哈數組

或者先看白話版好了 Compile - 白話版 bash

而後,generate的做用就是,解析 parse 生成的 ast 節點,拼接成字符串,而這個字符串,是能夠被轉化成函數執行的。函數執行後,會生成對應的 Vnode函數

Vnode 就是Vue 頁面的基礎,咱們就能夠根據他完成DOM 的建立和更新了學習

好比這樣ui

ast
{    

    tag:"div",    

    children:[],    

    attrsList:[{        

        name:111

    }]
}



拼接成函數

"_c('div', { attrs:{ name:111 } }, [])"



轉成函數

new Function(傳入上面的字符串)



生成 Vnode

{    

    tag: "div",    

    data:{        

        attrs: {name: "111"}

    },    

    children: undefined

}


複製代碼

本文概覽

本文主要講的是若是去把 生成好的 ast 拼接成 函數字符串(跟上面那個轉換同樣),而 ast 分爲不少種,而每一種的拼接方式都不同,咱們會針對每一種方式來詳細列出來this

下面將會講這麼多種類型節點的拼接spa

靜態節點,v-for 節點,v-if 節點,slot 節點,組件節點,子節點 等的拼接,內容較多卻不復雜,甚至有點有趣

那咱們就來看看 generate 自己的函數源碼先

比較簡短

function generate(ast, options) {    



    var state = new CodegenState(options);      

    var code = ast ? genElement(ast, state) : '_c("div")';    



    return {            

        render: "with(this){ return " + code + "}",            

        //專門用於存放靜態根節點的

        staticRenderFns: state.staticRenderFns
    }
}
複製代碼

對上面出現的幾個可能有點迷惑的東西解釋一下

參數 options

options 是傳入的一些判斷函數或者指令函數,以下,不一一列舉

{    

    expectHTML: true,    

    modules: modules$1,    

    directives: directives$1

    ....
};
複製代碼

函數 CodegenState

給該實例初始化編譯的狀態,下面會有源碼

函數 genElement

把 ast 轉成字符串的 罪魁禍首

generate 返回值

你也看到了

1 返回 genElement 拼接後的字符串code

這就是做爲 render 的主要形態,包了一層 with

render 會有一塊內容專門說,with 就很少說了哈,就是爲了爲 render 綁定實例爲上下文

2 返回 靜態根節點 的 靜態render

這是一個 數組,由於一個模板裏面可能存在多個靜態根節點,那麼就要把這些靜態根節點都轉換成 render 字符串保存起來,就是保存在數組中

上面是靜態根節點?簡單就是說,第一靜態,第二某一部分靜態節點的最大祖宗,以下圖

兩個 span 就是 靜態根節點,他們都是他們那個靜態部分的最大祖宗,而 div 下 有 v-if 的子節點,因此 div 不是靜態根節點

公衆號

而後下面這個靜態模板,解析獲得 render 放到 staticRenderFns 是這樣的

<div name="a">
    <span>111</span>
</div>

staticRenderFns=[
    `
     with(this){
        return _c('div',
            {attrs:{"name":"a"}},[111])]
        )
     }
    `
]
複製代碼

而 staticRenderFns 也會在 render 模塊下詳細記錄


CodegenState

初始化實例的編譯狀態

function CodegenState(options) {      

    this.options = options;      

    this.dataGenFns = [ klass$1.genData, style$1.genData];      

    this.directives = { on , bind, cloak, model,text ,html]

    this.staticRenderFns = [];
};
複製代碼

由於這個函數是給實例初始化一些屬性的,看到很明顯就是給實例添加上了不少屬性,this.xxxx 什麼的,那麼咱們就對 CodegenState 這個函數中添加的屬性解釋一下。

屬性 dataGenFns

這個數組,存放的是兩個函數

style$1.genData 處理 ast 中的 style ,包括動態靜態的 style

klass$1.genData 處理 ast 中的 class ,包括動態靜態的 class

好比

<div class="a" :class="aa" 

    style="height:0" :style="{width:0}">

</div>



解析成 ast

{    

    tag: "div",    

    type: 1,    

    staticStyle: "{"height":"0"}",    

    styleBinding: "{width:0}",    

    staticClass: ""a"",    

    classBinding: "name"

}



解析成字符串

`_c('div',{
    staticClass:"a",
    class:name,
    staticStyle:{"height":"0"},
    style:{width:0}
})
`

    staticClass:"a",
    class:name,
    staticStyle:{"height":"0"},
    style:{width:0}
})
`
複製代碼

dataGenFns 會在後面拼接節點數據的時候調用到

屬性 directives

這也是個數組,存放的是 Vue 自有指令的獨屬處理函數

包括如下幾個指令的處理函數

v-on,綁定事件

v-bind,綁定屬性

v-cloak,編譯前隱藏DOM

v-model,雙向綁定

v-text,插入文本

v-html,插入html

當你在模板中使用到以上的指令的時候,Vue 會調用相應的函數先進行處理

屬性 staticRenderFns

一個數組,用來存放靜態根節點的render 函數,上面有提到過一點

每一個實例都獨有這個屬性,若是沒有靜態根節點就爲空

好比下面這個模板,有兩個靜態根節點

公衆號

而後在實例 的 staticRenderFns 中就存放兩個 靜態 render

公衆號

那麼咱們如今就來看,generate 的重點函數,genElement


genElement

這是 ast 拼接成 字符串 的重點函數,主要是處理各類節點,而且拼接起來

靜態節點,v-for 節點,v-if 節點,slot 節點,組件節點,子節點 等,有一些省略了

能夠簡單看看下面的源碼

function genElement(el, state) {    

    if (

        el.staticRoot && !el.staticProcessed

    ) {        

        return genStatic(el, state)

    }    

    else if (

        el.for && !el.forProcessed

    ) {        

        return genFor(el, state)

    }    

    else if (

        el.if && !el.ifProcessed

    ) {        

        return genIf(el, state)

    }    

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

        return genSlot(el, state)

    }    

    else {    

    

        var code;  

     

        // 處理 is 綁定的組件
        if (el.component) {
            code = genComponent(el.component, el, state);
        }    



        // 上面全部的解析完以後,會走到這一步
        else {  

         

            // 當 el 不存在屬性的時候,el.plain = true

            var data = el.plain ? undefined : genData$2(el, state);  

          

            // 處理完父節點,遍歷處理全部子節點

            var children = genChildren(el, state);

            code = `_c(
                '${el.tag}'
                ${data ? ("," + data) : ''} 
                ${children ? ("," + children) : ''}
            )`
        }        



        return code

    }
}
複製代碼

重點是其中的各類處理函數,經過各類條件來選擇函數進行處理,而且會有一個 xxxProcessed 屬性,做用是證實是否已經進行過 xxx 方面的處理了,好比forProcessed = true,證實已經拼接過他的 v-for 了

在相應的函數中,會被這個屬性設置爲 true,而後遞歸的時候,就不會再調用相應的函數

以上的各類函數中會調用 genElement,以便遞歸處理其餘節點

genElement 按順序處理自身各類類型的節點後,開始 genData$2 拼接節點的數據,好比 attr ,prop 那些,而後再使用 genChildren 處理 子節點

拼接節點數據會在獨立一篇文章記錄,內容不少

下面咱們來一個個看其中涉及的節點處理函數


拼接靜態節點

function genStatic(el, state) {

    el.staticProcessed = true;

    state.staticRenderFns.push(
        "with(this){ return " + genElement(el, state) + "}" 
    );  



    return `
        _m(${
            state.staticRenderFns.length - 1
        })
    `

}
複製代碼

太簡單了,給一個模板看一下就能夠了

公衆號

處理完,存儲靜態render,並返回字符串 "_m(0)" , 很簡短吼

意思就是獲取 staticRenderFns 中的第一個值

其中的值,也是調用 genElement 獲得的靜態 render


拼接 vIf 節點

專門用於處理帶有 v-if 的節點

function genIf(
    el, state) {

    el.ifProcessed = true; // 避免遞歸

    return genIfConditions(
        el.ifConditions.slice(),
        state
    )
}

複製代碼

看到 parse 文章的,想必應該知道 el.ifCondition 是什麼了吧

簡單說一下吧,el.ifCondition 是用來存放條件節點的數組

什麼叫條件節點啊?

好比 你有這樣的模板

公衆號

像 上面的 p,span,section 三個節點都是條件節點,不會直接存放到父節點的 children 中

由於並非立刻顯示的

而後他們解析獲得的 ast ,都會被存放到 p 節點的 ifCondition 中

像這樣

{    

    tag:"div",    

    children:[{        

        tag:"p",        

        ifCondition:[{            

            exp: "isShow",            

            block: {..p 的 ast 節點}

        },{            

            exp: "isShow==2",            

            block: {..span 的 ast 節點}

        },{            

            exp: undefined,            

            block: {..section 的 ast 節點}

        }]
    }]
}
複製代碼

el.ifCondition 就是把 這個數組複製一遍(我又學到了,以前我並不知道能夠這麼去複製數組)

而後傳給 genIfCondition,看看源碼

function genIfConditions(
    conditions, state,

) {    



    // 當 沒有條件的時候,就返回 _e ,意思是空節點

    if (!conditions.length) {        

        return  '_e()' 

    }  



    // 遍歷一遍以後,就把條件剔除

    var condition = conditions.shift();  



    if (condition.exp) {        

        return (

            condition.exp + "?" +
            genElement(condition.block,state) + ":" +
            genIfConditions(conditions, state )
        )
    } else {        

        return genElement(condition.block,state)

    }
}
複製代碼

這個函數的做用呢,是這樣的

一、按順序處理 ifCondition 中的每個節點,而且會移出數組

二、而且每個節點使用 三元表達式 去拼接

三、遞歸調用 genIfConditions 去處理剩下的 ifCondition

按下面的模板來講明一下流程

公衆號

第一輪

ifCondition = [ p,span,section ]

獲取 ifCondition 第一個節點,也就是p,並移出 ifCondition 數組

此時 ifCondition = [ span,section ]

p 節點有表達式 isShow,須要三元表達式拼接,變成

" isShow ? _c('p') : genIfConditions( 剩下的 ifCondition )"
複製代碼

第二輪

genIfConditions 一樣獲取第一個節點,span

此時 ifCondition = [ section ]

span 有表達式 isShow==2,須要拼接三元表達式,變成

" isShow ? _c('p') : ( isShow==2 ? _c( 'span') : genIfConditions( 剩下的 ifCondition ) )"
複製代碼

第三輪

genIfConditions 一樣獲取第一個節點,section

此時 ifCondition = [ ]

section 沒有表達式,直接處理節點,拼接成

" isShow ? _c('p') : ( isShow==2 ? _c( 'span') : _c( 'section') )"
複製代碼

而後就處理完啦,上面的字符串,就是 genIf 處理後拼接上的字符串

接下來看是怎麼拼接帶有v-for 的指令的


拼接v-for 節點

function genFor(
    el, state

) {    



    var exp = el.for;    

    var alias = el.alias;    

    var iterator1 = el.iterator1 ? ("," + el.iterator1 ) : '';    

    var iterator2 = el.iterator2 ? ("," + el.iterator2 ) : '';

    el.forProcessed = true; // avoid recursion



    return  (        

        '_l (' + exp + ",function(" + alias + iterator1 + iterator2 + "){" +            

                "return " + genElement(el, state) +        

        '})'

    )
}
複製代碼

你們應該均可以看得懂的吧,給個例子

公衆號

`_c('div', _l( arr ,function(item,index){ return _c('span') } )`
複製代碼

就這樣,v-for 就解析成了一個 _l 函數,這個函數會遍歷 arr,遍歷一遍,就生成一個節點

下面在看看是如何處理子節點的


拼接子節點

function genChildren(el, state) {    



    var children = el.children;    



    if (!children.length) return


    return` [$ {
        children.map(function(c) {

            if (node.type === 1) {
                return genElement(node, state)
            }

            return`_v($ {
                text.type === 2 
                ? text.expression
                : ("'" + text.text + "'")
            })`
        }).join(','))
    }]`

}
複製代碼

一樣的,這個函數也是很簡單的吼

就是遍歷全部子節點,逐個處理子節點,而後獲得一個新的數組

一、當子節點 type ==1 時,說明是標籤,那麼就要 genElement 處理一遍

二、不然,就是文本節點

若是 type =2 ,那麼是表達式文本,不然,就是普通文本

普通文本,須要左右兩邊加引號。表達式是個變量,須要在實例上獲取,因此不用加雙引號

舉個例子

公衆號

解析成字符串

`_c('div',[ _c('span') ,_c('section') ,_c('a') ])`
複製代碼

拼接插槽

function genSlot(el, state) {    



    var slotName = el.slotName || '"default"';    



    var children = genChildren(el, state);    

    var res = `
        _t( ${slotName} ,

            ${ children ? ("," + children) : ''}

        )
    `

    var attrs =
        el.attrs && "{" +
            el.attrs.map(function(a) {                

                return camelize(a.name) + ":" + a.value;

            }).join(',') +        

        "}";    



    if (attrs  && !children) {

        res += ",null";
    }    
    // _t 的參數順序是 slotName, children,attrs,bind
    if (attrs) {
        res += "," + attrs;
    }    

    return res + ')'

}
複製代碼

genSlot 主要是處理子節點 和 綁定在 slot 上的 attrs

屬性 attrs 會逐個拼接成 xx:xx 的形式 ,合成一個新的數組,而後經過 逗號隔開成字符串

原 attrs = [
    { name:"a-a" ,value:"aaa"},
    { name:"b-b" ,value:"bbb"}

]



轉換後,name 會變成駝峯

attrs = 'aA:"aaa", bB:"bbb"'
複製代碼

看下例子,一個slot,綁定屬性 a 做爲 scope,而且有 span 做爲默認內容

公衆號

解析成這樣

_c('div',[_t("default", [_c('span')] ,{a:aa} )] )
複製代碼

而後剩最後一個了,解析組件的節點


拼接組件

function genComponent(componentName, el, state) {    



    var children = genChildren(el, state, true);    



    return `_c(        

        ${componentName},        

        ${genData$2(el, state)}
        ${children ? ("," + children) : ''}
    )`

}
複製代碼

其實,解析組件,就是把他先當作普通標籤進行處理,在這裏並無作什麼特殊的操做

可是這個方法是用來處理 【帶有 is 屬性】 的節點

不然 就不會存在 el.component 這個屬性,就不會調用 genComponent

公衆號

拼接成下面這樣,而其中的 is 屬性的拼接在 下篇文章 genData$2 中會有說明

`_c('div',[_c("test",{tag:"a"})])`
複製代碼

那若是直接寫組件名做爲標籤,是怎麼處理?

也沒有作什麼特殊處理,具體看 genElement 最後那段

公衆號

一樣當作普通標籤先解析

看個例子

公衆號

解析成這樣的字符串

`_c('div',[ _c('test', [_c('span')] )] )`
複製代碼

走個流程

看了上面這麼多的處理函數,各類函數處理後獲得的字符串是相加的關係

而後如今用一個小例子來實現如下拼接步驟

公衆號

一、先解析最外層 div,獲得字符串

`_c( 'div'  ` 
複製代碼

genChildren 開始解析子節點

二、處理 strong,這是一個靜態根節點,genStatic 處理獲得字符串

`_c( 'div' , [ _m(0) `
複製代碼

三、處理 p 節點,genIf 處理拼接字符串

`_c( 'div' , [ _m(0) , isShow? _c(p) :_e() `
複製代碼

四、處理 span 節點, genFor 拼接字符串

`_c( 'div' , [
    _m(0) ,
    isShow? _c(p) :_e() ,
    _l(arr,function(item,index){return _c('span')})
`
複製代碼

五、處理 test 組件節點,genComponent 拼接

`_c( 'div' , [
    _m(0) ,
    isShow? _c(p) :_e() ,
    _l(arr,function(item,index){return _c('span')}),
    _c('test')
`
複製代碼

六、genChildren 處理完全部子節點拼接上末尾的括號獲得

`_c( 'div' , [
    _m(0) ,
    isShow? _c(p) :_e() ,
    _l(arr,function(item,index){return _c('span')}),
    _c('test')
])
`
複製代碼

而後整個genElement 流程就處理完了

上面獲得的 字符串,只要轉換成函數,就能夠執行了,因而也就能夠獲得咱們的 Vnode


感悟

有時你會想,看這個東西有什麼用啊,其實你只作正常項目的話,你的確大可沒必要去看這部分的內容,可是若是你真的想成竹在胸,百分百掌握Vue,你就必須看,由於你能夠作更多東西

好比以前接了個外包,要根據別人打包好的文件,去還原別人的源碼!

難度之大之複雜,你也想得出來,不過幸虧我看過源碼,打包後的文件,模板全是 render 函數,因此我能夠手動還原出來原始模板!

雖然我也能夠寫一個 反編譯模板函數,可是工做量太大,沒得想法了。還原的難度就在於 render 變成模板了,由於其餘的什麼 method 等是原封不動的哈哈,但是直接照抄

公衆號


最後

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

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