Vue render函數(shù)

前幾天想學(xué)學(xué)Vue中怎么編寫可復(fù)用的組件,提到要對(duì)Vue的render函數(shù)有所了解。可仔細(xì)一想,對(duì)于Vue的render函數(shù)自己只是看了官方的一些介紹,并未深入一點(diǎn)去了解這方面的知識(shí)。為了更好的學(xué)習(xí)后續(xù)的知識(shí),又折回來(lái)了解Vue中的render函數(shù),這一切主要都是為了后續(xù)能更好的學(xué)習(xí)Vue的知識(shí)。

回憶Vue的一些基本概念

今天我們學(xué)習(xí)的目的是了解和學(xué)習(xí)Vue的render函數(shù)。如果想要更好的學(xué)習(xí)Vue的render函數(shù)相關(guān)的知識(shí),我們有必要重溫一下Vue中的一些基本概念。那么先上一張圖,這張圖從宏觀上展現(xiàn)了Vue整體流程:

image

從上圖中,不難發(fā)現(xiàn)一個(gè)Vue的應(yīng)用程序是如何運(yùn)行起來(lái)的,模板通過(guò)編譯生成AST,再由AST生成Vue的render函數(shù)(渲染函數(shù)),渲染函數(shù)結(jié)合數(shù)據(jù)生成Virtual DOM樹,Diff和Patch后生成新的UI。從這張圖中,可以接觸到Vue的一些主要概念:

  • 模板:Vue的模板基于純HTML,基于Vue的模板語(yǔ)法,我們可以比較方便地聲明數(shù)據(jù)和UI的關(guān)系。
  • AST:AST是Abstract Syntax Tree的簡(jiǎn)稱,Vue使用HTML的Parser將HTML模板解析為AST,并且對(duì)AST進(jìn)行一些優(yōu)化的標(biāo)記處理,提取最大的靜態(tài)樹,方便Virtual DOM時(shí)直接跳過(guò)Diff。
  • 渲染函數(shù):渲染函數(shù)是用來(lái)生成Virtual DOM的。Vue推薦使用模板來(lái)構(gòu)建我們的應(yīng)用界面,在底層實(shí)現(xiàn)中Vue會(huì)將模板編譯成渲染函數(shù),當(dāng)然我們也可以不寫模板,直接寫渲染函數(shù),以獲得更好的控制 (這部分是我們今天主要要了解和學(xué)習(xí)的部分)。
  • Virtual DOM:虛擬DOM樹,Vue的Virtual DOM Patching算法是基于Snabbdom的實(shí)現(xiàn),并在些基礎(chǔ)上作了很多的調(diào)整和改進(jìn)。
  • Watcher:每個(gè)Vue組件都有一個(gè)對(duì)應(yīng)的watcher,這個(gè)watcher將會(huì)在組件render的時(shí)候收集組件所依賴的數(shù)據(jù),并在依賴有更新的時(shí)候,觸發(fā)組件重新渲染。你根本不需要寫shouldComponentUpdate,Vue會(huì)自動(dòng)優(yōu)化并更新要更新的UI。

上圖中,render函數(shù)可以作為一道分割線,render函數(shù)的左邊可以稱之為編譯期,將Vue的模板轉(zhuǎn)換為渲染函數(shù)render函數(shù)的右邊是Vue的運(yùn)行時(shí),主要是基于渲染函數(shù)生成Virtual DOM樹,Diff和Patch。

渲染函數(shù)的基礎(chǔ)

Vue推薦在絕大多數(shù)情況下使用template來(lái)創(chuàng)建你的HTML。然而在一些場(chǎng)景中,需要使用JavaScript的編程能力和創(chuàng)建HTML,這就是render函數(shù),它比template更接近編譯器。

<h1>
    <a name="hello-world" href="#hello-world">
        Hello world!
    </a>
</h1>

在HTML層,我們決定這樣定義組件接口:

<anchored-heading :level="1">Hello world!</anchored-heading>

當(dāng)我們開始寫一個(gè)通過(guò)levelprop動(dòng)態(tài)生成heading標(biāo)簽的組件,你可能很快想到這樣實(shí)現(xiàn):

<!-- HTML -->
<script type="text/x-template" id="anchored-heading-template">
    <h1 v-if="level === 1">
        <slot></slot>
    </h1>
    <h2 v-else-if="level === 2">
        <slot></slot>
    </h2>
    <h3 v-else-if="level === 3">
        <slot></slot>
    </h3>
    <h4 v-else-if="level === 4">
        <slot></slot>
    </h4>
    <h5 v-else-if="level === 5">
        <slot></slot>
    </h5>
    <h6 v-else-if="level === 6">
        <slot></slot>
    </h6>
</script>

<!-- Javascript -->
Vue.component('anchored-heading', {
    template: '#anchored-heading-template',
    props: {
        level: {
            type: Number,
            required: true
        }
    }
})

在這種場(chǎng)景中使用 template 并不是最好的選擇:首先代碼冗長(zhǎng),為了在不同級(jí)別的標(biāo)題中插入錨點(diǎn)元素,我們需要重復(fù)地使用 <slot></slot>

雖然模板在大多數(shù)組件中都非常好用,但是在這里它就不是很簡(jiǎn)潔的了。那么,我們來(lái)嘗試使用 render 函數(shù)重寫上面的例子:

Vue.component('anchored-heading', {
    render: function (createElement) {
        return createElement(
            'h' + this.level,   // tag name 標(biāo)簽名稱
            this.$slots.default // 子組件中的陣列
        )
    },
    props: {
        level: {
            type: Number,
            required: true
        }
    }
})

簡(jiǎn)單清晰很多!簡(jiǎn)單來(lái)說(shuō),這樣代碼精簡(jiǎn)很多,但是需要非常熟悉 Vue 的實(shí)例屬性。在這個(gè)例子中,你需要知道當(dāng)你不使用 slot 屬性向組件中傳遞內(nèi)容時(shí),比如 anchored-heading 中的 Hello world!,這些子元素被存儲(chǔ)在組件實(shí)例中的 $slots.default中。

節(jié)點(diǎn)、樹以及虛擬DOM

對(duì)Vue的一些概念和渲染函數(shù)的基礎(chǔ)有一定的了解之后,我們需要對(duì)一些瀏覽器的工作原理有一些了解,這樣對(duì)我們學(xué)習(xí)render函數(shù)是很重要的。比如下面的這段HTML代碼:

<div>
    <h1>My title</h1>
    Some text content
    <!-- TODO: Add tagline -->
</div>

當(dāng)瀏覽器讀到這些代碼時(shí),它會(huì)建立一個(gè)DOM節(jié)點(diǎn)樹來(lái)保持追蹤,如果你會(huì)畫一張家譜樹來(lái)追蹤家庭成員的發(fā)展一樣。

HTML的DOM節(jié)點(diǎn)樹如下圖所示:

image

每個(gè)元素都是一個(gè)節(jié)點(diǎn)。每片文字也是一個(gè)節(jié)點(diǎn)。甚至注釋也都是節(jié)點(diǎn)。一個(gè)節(jié)點(diǎn)就是頁(yè)面的一個(gè)部分。就像家譜樹一樣,每個(gè)節(jié)點(diǎn)都可以有孩子節(jié)點(diǎn) (也就是說(shuō)每個(gè)部分可以包含其它的一些部分)。

高效的更新所有這些節(jié)點(diǎn)會(huì)是比較困難的,不過(guò)所幸你不必再手動(dòng)完成這個(gè)工作了。你只需要告訴 Vue 你希望頁(yè)面上的 HTML 是什么,這可以是在一個(gè)模板里:

<h1>{{ blogTitle }}</h1>

或者一個(gè)渲染函數(shù)里:

render: function (createElement) {
    return createElement('h1', this.blogTitle)
}

在這兩種情況下,Vue 都會(huì)自動(dòng)保持頁(yè)面的更新,即便 blogTitle 發(fā)生了改變。

虛擬DOM

在Vue 2.0中,渲染層的實(shí)現(xiàn)做了根本性改動(dòng),那就是引入了虛擬DOM。

image

Vue的編譯器在編譯模板之后,會(huì)把這些模板編譯成一個(gè)渲染函數(shù)。而函數(shù)被調(diào)用的時(shí)候就會(huì)渲染并且返回一個(gè)虛擬DOM的樹

當(dāng)我們有了這個(gè)虛擬的樹之后,再交給一個(gè)Patch函數(shù),負(fù)責(zé)把這些虛擬DOM真正施加到真實(shí)的DOM上。在這個(gè)過(guò)程中,Vue有自身的響應(yīng)式系統(tǒng)來(lái)偵測(cè)在渲染過(guò)程中所依賴到的數(shù)據(jù)來(lái)源。在渲染過(guò)程中,偵測(cè)到數(shù)據(jù)來(lái)源之后就可以精確感知數(shù)據(jù)源的變動(dòng)。到時(shí)候就可以根據(jù)需要重新進(jìn)行渲染。當(dāng)重新進(jìn)行渲染之后,會(huì)生成一個(gè)新的樹,將新的樹與舊的樹進(jìn)行對(duì)比,就可以最終得出應(yīng)施加到真實(shí)DOM上的改動(dòng)。最后再通過(guò)Patch函數(shù)施加改動(dòng)。

簡(jiǎn)單點(diǎn)講,在Vue的底層實(shí)現(xiàn)上,Vue將模板編譯成虛擬DOM渲染函數(shù)。結(jié)合Vue自帶的響應(yīng)系統(tǒng),在應(yīng)該狀態(tài)改變時(shí),Vue能夠智能地計(jì)算出重新渲染組件的最小代價(jià)并應(yīng)到DOM操作上。

image

Vue支持我們通過(guò)data參數(shù)傳遞一個(gè)JavaScript對(duì)象做為組件數(shù)據(jù),然后Vue將遍歷此對(duì)象屬性,使用Object.defineProperty方法設(shè)置描述對(duì)象,通過(guò)存取器函數(shù)可以追蹤該屬性的變更,Vue創(chuàng)建了一層Watcher層,在組件渲染的過(guò)程中把屬性記錄為依賴,之后當(dāng)依賴項(xiàng)的setter被調(diào)用時(shí),會(huì)通知Watcher重新計(jì)算,從而使它關(guān)聯(lián)的組件得以更新,如下圖:

image

有關(guān)于Vue的響應(yīng)式相關(guān)的內(nèi)容,可以閱讀下列文章:

對(duì)于Vue自帶的響應(yīng)式系統(tǒng),并不是咱們今天要聊的東西。我們還是回到Vue的虛擬DOM中來(lái)。對(duì)于虛擬DOM,咱們來(lái)看一個(gè)簡(jiǎn)單的實(shí)例,就是下圖所示的這個(gè),詳細(xì)的闡述了模板 → 渲染函數(shù) → 虛擬DOM樹 → 真實(shí)DOM的一個(gè)過(guò)程

image

其實(shí)Vue中的虛擬DOM還是很復(fù)雜的,我也是一知半解,如果你想深入的了解,可以閱讀@JoeRay61的《Vue原理解析之Virtual DOM》一文。

通過(guò)前面的學(xué)習(xí),我們初步了解到Vue通過(guò)建立一個(gè)虛擬DOM對(duì)真實(shí)DOM發(fā)生的變化保持追蹤。比如下面這行代碼:

return createElement('h1', this.blogTitle)

createElement 到底會(huì)返回什么呢?其實(shí)不是一個(gè)實(shí)際的 DOM 元素。它更準(zhǔn)確的名字可能是 createNodeDescription,因?yàn)樗男畔?huì)告訴 Vue 頁(yè)面上需要渲染什么樣的節(jié)點(diǎn),及其子節(jié)點(diǎn)。我們把這樣的節(jié)點(diǎn)描述為“虛擬節(jié)點(diǎn) (Virtual Node)”,也常簡(jiǎn)寫它為“VNode”。“虛擬 DOM”是我們對(duì)由 Vue 組件樹建立起來(lái)的整個(gè) VNode 樹的稱呼。

Vue組件樹建立起來(lái)的整個(gè)VNode樹是唯一的。這意味著,下面的render函數(shù)是無(wú)效的:

render: function (createElement) {
    var myParagraphVNode = createElement('p', 'hi')
    return createElement('div', [
        // 錯(cuò)誤-重復(fù)的 VNodes
        myParagraphVNode, myParagraphVNode
    ])
}

如果你真的需要重復(fù)很多次的元素/組件,你可以使用工廠函數(shù)來(lái)實(shí)現(xiàn)。例如,下面這個(gè)例子 render 函數(shù)完美有效地渲染了 20 個(gè)重復(fù)的段落:

render: function (createElement) {
    return createElement('div',
        Array.apply(null, { length: 20 }).map(function () {
            return createElement('p', 'hi')
        })
    )
}

Vue的渲染機(jī)制

image

上圖展示的是獨(dú)立構(gòu)建時(shí)的一個(gè)渲染流程圖。

繼續(xù)使用上面用到的模板到真實(shí)DOM過(guò)程的一個(gè)圖:

image

這里會(huì)涉及到Vue的另外兩個(gè)概念:

  • 獨(dú)立構(gòu)建:包含模板編譯器,渲染過(guò)程HTML字符串 → render函數(shù) → VNode → 真實(shí)DOM節(jié)點(diǎn)
  • 運(yùn)行時(shí)構(gòu)建:不包含模板編譯器,渲染過(guò)程render函數(shù) → VNode → 真實(shí)DOM節(jié)點(diǎn)

運(yùn)行時(shí)構(gòu)建的包,會(huì)比獨(dú)立構(gòu)建少一個(gè)模板編譯器。在$mount函數(shù)上也不同。而$mount方法又是整個(gè)渲染過(guò)程的起始點(diǎn)。用一張流程圖來(lái)說(shuō)明:

image

由此圖可以看到,在渲染過(guò)程中,提供了三種渲染模式,自定義render函數(shù)、templateel均可以渲染頁(yè)面,也就是對(duì)應(yīng)我們使用Vue時(shí),三種寫法:

自定義render函數(shù)

Vue.component('anchored-heading', {
    render: function (createElement) {
        return createElement (
            'h' + this.level,   // tag name標(biāo)簽名稱
            this.$slots.default // 子組件中的陣列
        )
    },
    props: {
        level: {
            type: Number,
            required: true
        }
    }
})

template寫法

let app = new Vue({
    template: `<div>{{ msg }}</div>`,
    data () {
        return {
            msg: ''
        }
    }
})

el寫法

let app = new Vue({
    el: '#app',
    data () {
        return {
            msg: 'Hello Vue!'
        }
    }
})

這三種渲染模式最終都是要得到render函數(shù)。只不過(guò)用戶自定義的render函數(shù)省去了程序分析的過(guò)程,等同于處理過(guò)的render函數(shù),而普通的template或者el只是字符串,需要解析成AST,再將AST轉(zhuǎn)化為render函數(shù)。

記住一點(diǎn),無(wú)論哪種方法,都要得到render函數(shù)。

我們?cè)谑褂眠^(guò)程中具體要使用哪種調(diào)用方式,要根據(jù)具體的需求來(lái)。

如果是比較簡(jiǎn)單的邏輯,使用templateel比較好,因?yàn)檫@兩種都屬于聲明式渲染,對(duì)用戶理解比較容易,但靈活性比較差,因?yàn)樽罱K生成的render函數(shù)是由程序通過(guò)AST解析優(yōu)化得到的;而使用自定義render函數(shù)相當(dāng)于人已經(jīng)將邏輯翻譯給程序,能夠勝任復(fù)雜的邏輯,靈活性高,但對(duì)于用戶的理解相對(duì)差點(diǎn)。

理解createElement

在使用render函數(shù),其中還有另一個(gè)需要掌握的部分,那就是createElement。接下來(lái)我們需要熟悉的是如何在createElement函數(shù)中生成模板。那么我們分兩個(gè)部分來(lái)對(duì)createElement進(jìn)行理解。

createElement參數(shù)

createElement可以是接受多個(gè)參數(shù):

第一個(gè)參數(shù):{String | Object | Function}

第一個(gè)參數(shù)對(duì)于createElement而言是一個(gè)必須的參數(shù),這個(gè)參數(shù)可以是字符串string、是一個(gè)對(duì)象object,也可以是一個(gè)函數(shù)function

<div id="app">
    <custom-element></custom-element>
</div>

Vue.component('custom-element', {
    render: function (createElement) {
        return createElement('div')
    }
})

let app = new Vue({
    el: '#app'
})

上面的示例,給createElement傳了一個(gè)String參數(shù)'div',即傳了一個(gè)HTML標(biāo)簽字符。最后會(huì)有一個(gè)div元素渲染出來(lái):

image

接著把上例中的String換成一個(gè)Object,比如:

Vue.component('custom-element', {
    render: function (createElement) {
        return createElement({
            template: `<div>Hello Vue!</div>`
        })
    }
})

上例傳了一個(gè){template: '<div>Hello Vue!</div>'}對(duì)象。此時(shí)custom-element組件渲染出來(lái)的結(jié)果如下:

image

除此之外,還可以傳一個(gè)Function,比如:

Vue.component('custom-element', {
    render: function (createElement) {
        var eleFun = function () {
            return {
                template: `<div>Hello Vue!</div>`
            }
        }
        return createElement(eleFun())
    }
})

最終得到的結(jié)果和上圖是一樣的。這里傳了一個(gè)eleFun()函數(shù)給createElement,而這個(gè)函數(shù)返回的是一個(gè)對(duì)象。

第二個(gè)參數(shù):{Object}

createElement是一個(gè)可選參數(shù),這個(gè)參數(shù)是一個(gè)Object。來(lái)看一個(gè)小示例:

<div id="app">
    <custom-element></custom-element>
</div>

Vue.component('custom-element', {
    render: function (createElement) {
        var self = this

        // 第一個(gè)參數(shù)是一個(gè)簡(jiǎn)單的HTML標(biāo)簽字符 “必選”
        // 第二個(gè)參數(shù)是一個(gè)包含模板相關(guān)屬性的數(shù)據(jù)對(duì)象 “可選”
        return createElement('div', {
            'class': {
                foo: true,
                bar: false
            },
            style: {
                color: 'red',
                fontSize: '14px'
            },
            attrs: {
                id: 'boo'
            },
            domProps: {
                innerHTML: 'Hello Vue!'
            }
        })
    }
})

let app = new Vue({
    el: '#app'
})

最終生成的DOM,將會(huì)帶一些屬性和內(nèi)容的div元素,如下圖所示:

image

第三個(gè)參數(shù):{String | Array}

createElement還有第三個(gè)參數(shù),這個(gè)參數(shù)是可選的,可以給其傳一個(gè)StringArray。比如下面這個(gè)小示例:

<div id="app">
    <custom-element></custom-element>
</div>

Vue.component('custom-element', {
    render: function (createElement) {
        var self = this

        return createElement(
            'div', // 第一個(gè)參數(shù)是一個(gè)簡(jiǎn)單的HTML標(biāo)簽字符 “必選”
            {
                class: {
                    title: true
                },
                style: {
                    border: '1px solid',
                    padding: '10px'
                }
            }, // 第二個(gè)參數(shù)是一個(gè)包含模板相關(guān)屬性的數(shù)據(jù)對(duì)象 “可選”
            [
                createElement('h1', 'Hello Vue!'),
                createElement('p', '開始學(xué)習(xí)Vue!')
            ] // 第三個(gè)參數(shù)是傳了多個(gè)子元素的一個(gè)數(shù)組 “可選”
        )
    }
})

let app = new Vue({
    el: '#app'
})

最終的效果如下:

image

其實(shí)從上面這幾個(gè)小例來(lái)看,不難發(fā)現(xiàn),以往我們使用Vue.component()創(chuàng)建組件的方式,都可以用render函數(shù)配合createElement來(lái)完成。你也會(huì)發(fā)現(xiàn),使用Vue.component()render各有所長(zhǎng),正如文章開頭的一個(gè)示例代碼,就不適合Vue.component()template,而使用render更方便。

接下來(lái)看一個(gè)小示例,看看templaterender方式怎么創(chuàng)建相同效果的一個(gè)組件:

<div id="app">
    <custom-element></custom-element>
</div>

Vue.component('custom-element', {
    template: `<div id="box" :class="{show: show}" @click="handleClick">Hello Vue!</div>`,
    data () {
        return {
            show: true
        }
    },
    methods: {
        handleClick: function () {
            console.log('Clicked!')
        }
    }
})

上面Vue.component()中的代碼換成render函數(shù)之后,可以這樣寫:

Vue.component('custom-element', {
    render: function (createElement) {
        return createElement('div', {
            class: {
                show: this.show
            },
            attrs: {
                id: 'box'
            },
            on: {
                click: this.handleClick
            }
        }, 'Hello Vue!')
    },
    data () {
        return {
            show: true
        }
    },
    methods: {
        handleClick: function () {
            console.log('Clicked!')
        }
    }
})

最后聲明一個(gè)Vue實(shí)例,并掛載到id#app的一個(gè)元素上:

let app = new Vue({
    el: '#app'
})

createElement解析過(guò)程

簡(jiǎn)單的來(lái)看一下createElement解析的過(guò)程,這部分需要對(duì)JS有一些功底。不然看起來(lái)有點(diǎn)蛋疼:

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {

    // 兼容不傳data的情況
    if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children
        children = data
        data = undefined
    }

    // 如果alwaysNormalize是true
    // 那么normalizationType應(yīng)該設(shè)置為常量ALWAYS_NORMALIZE的值
    if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
        // 調(diào)用_createElement創(chuàng)建虛擬節(jié)點(diǎn)
        return _createElement(context, tag, data, children, normalizationType)
    }

    function _createElement (context, tag, data, children, normalizationType) {
        /**
        * 如果存在data.__ob__,說(shuō)明data是被Observer觀察的數(shù)據(jù)
        * 不能用作虛擬節(jié)點(diǎn)的data
        * 需要拋出警告,并返回一個(gè)空節(jié)點(diǎn)
        * 
        * 被監(jiān)控的data不能被用作vnode渲染的數(shù)據(jù)的原因是:
        * data在vnode渲染過(guò)程中可能會(huì)被改變,這樣會(huì)觸發(fā)監(jiān)控,導(dǎo)致不符合預(yù)期的操作
        */
        if (data && data.__ob__) {
            process.env.NODE_ENV !== 'production' && warn(
            `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
            'Always create fresh vnode data objects in each render!',
            context
            )
            return createEmptyVNode()
        }

        // 當(dāng)組件的is屬性被設(shè)置為一個(gè)falsy的值
        // Vue將不會(huì)知道要把這個(gè)組件渲染成什么
        // 所以渲染一個(gè)空節(jié)點(diǎn)
        if (!tag) {
            return createEmptyVNode()
        }

        // 作用域插槽
        if (Array.isArray(children) && typeof children[0] === 'function') {
            data = data || {}
            data.scopedSlots = { default: children[0] }
            children.length = 0
        }

        // 根據(jù)normalizationType的值,選擇不同的處理方法
        if (normalizationType === ALWAYS_NORMALIZE) {
            children = normalizeChildren(children)
        } else if (normalizationType === SIMPLE_NORMALIZE) {
            children = simpleNormalizeChildren(children)
        }
        let vnode, ns

        // 如果標(biāo)簽名是字符串類型
        if (typeof tag === 'string') {
            let Ctor
            // 獲取標(biāo)簽名的命名空間
            ns = config.getTagNamespace(tag)

            // 判斷是否為保留標(biāo)簽
            if (config.isReservedTag(tag)) {
                // 如果是保留標(biāo)簽,就創(chuàng)建一個(gè)這樣的vnode
                vnode = new VNode(
                    config.parsePlatformTagName(tag), data, children,
                    undefined, undefined, context
                )

                // 如果不是保留標(biāo)簽,那么我們將嘗試從vm的components上查找是否有這個(gè)標(biāo)簽的定義
            } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
                // 如果找到了這個(gè)標(biāo)簽的定義,就以此創(chuàng)建虛擬組件節(jié)點(diǎn)
                vnode = createComponent(Ctor, data, context, children, tag)
            } else {
                // 兜底方案,正常創(chuàng)建一個(gè)vnode
                vnode = new VNode(
                    tag, data, children,
                    undefined, undefined, context
                )
            }

        // 當(dāng)tag不是字符串的時(shí)候,我們認(rèn)為tag是組件的構(gòu)造類
        // 所以直接創(chuàng)建
        } else {
            vnode = createComponent(tag, data, context, children)
        }

        // 如果有vnode
        if (vnode) {
            // 如果有namespace,就應(yīng)用下namespace,然后返回vnode
            if (ns) applyNS(vnode, ns)
            return vnode
        // 否則,返回一個(gè)空節(jié)點(diǎn)
        } else {
            return createEmptyVNode()
        }
    }
}

簡(jiǎn)單的梳理了一個(gè)流程圖,可以參考下

image

這部分代碼和流程圖來(lái)自于@JoeRay61的《Vue原理解析之Virtual DOM》一文。

使用JavaScript代替模板功能

在使用Vue模板的時(shí)候,我們可以在模板中靈活的使用v-ifv-forv-model<slot>之類的。但在render函數(shù)中是沒有提供專用的API。如果在render使用這些,需要使用原生的JavaScript來(lái)實(shí)現(xiàn)。

v-ifv-for

render函數(shù)中可以使用if/elsemap來(lái)實(shí)現(xiàn)template中的v-ifv-for

<ul v-if="items.length">
    <li v-for="item in items">{{ item }}</li>
</ul>
<p v-else>No items found.</p>

換成render函數(shù),可以這樣寫:

Vue.component('item-list',{
    props: ['items'],
    render: function (createElement) {
        if (this.items.length) {
            return createElement('ul', this.items.map(item => createElement('li',item)))
        } else {
            return createElement('p', 'No items found.')
        }
    }
})

<div id="app">
    <item-list :items="items"></item-list>
</div>

let app = new Vue({
    el: '#app',
    data () {
        return {
            items: ['大漠', 'W3cplus', 'blog']
        }
    }
})

得到的效果如下:

image

v-model

render函數(shù)中也沒有與v-model相應(yīng)的API,如果要實(shí)現(xiàn)v-model類似的功能,同樣需要使用原生JavaScript來(lái)實(shí)現(xiàn)。

<div id="app">
    <el-input :name="name" @input="val => name = val"></el-input>
</div>

Vue.component('el-input', {
    render: function (createElement) {
        var self = this
        return createElement('input', {
            domProps: {
                value: self.name
            },
            on: {
                input: function (event) {
                    self.$emit('input', event.target.value)
                }
            }
        })
    },
    props: {
        name: String
    }
})

let app = new Vue({
    el: '#app',
    data () {
        return {
            name: '大漠'
        }
    }
})

刷新你的瀏覽器,可以看到效果如下:

image

這就是深入底層要付出的,盡管麻煩了一些,但相對(duì)于 v-model 來(lái)說(shuō),你可以更靈活地控制。

插槽

你可以從this.$slots獲取VNodes列表中的靜態(tài)內(nèi)容:

render: function (createElement) {
    // 相當(dāng)于 `<div><slot></slot></div>`
    return createElement('div', this.$slots.default)
}

還可以從this.$scopedSlots中獲得能用作函數(shù)的作用域插槽,這個(gè)函數(shù)返回VNodes:

props: ['message'],
render: function (createElement) {
    // `<div><slot :text="message"></slot></div>`
    return createElement('div', [
        this.$scopedSlots.default({
            text: this.message
        })
    ])
}

如果要用渲染函數(shù)向子組件中傳遞作用域插槽,可以利用VNode數(shù)據(jù)中的scopedSlots域:

<div id="app">
    <custom-ele></custom-ele>
</div>

Vue.component('custom-ele', {
    render: function (createElement) {
        return createElement('div', [
            createElement('child', {
                scopedSlots: {
                    default: function (props) {
                        return [
                            createElement('span', 'From Parent Component'),
                            createElement('span', props.text)
                        ]
                    }
                }
            })
        ])
    }
})

Vue.component('child', {
    render: function (createElement) {
        return createElement('strong', this.$scopedSlots.default({
            text: 'This is Child Component'
        }))
    }
})

let app = new Vue({
    el: '#app'
})

JSX

如果寫習(xí)慣了template,然后要用render函數(shù)來(lái)寫,一定會(huì)感覺好痛苦,特別是面對(duì)復(fù)雜的組件的時(shí)候。不過(guò)我們?cè)赩ue中使用JSX可以讓我們回到更接近于模板的語(yǔ)法上。

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
    el: '#demo',
    render: function (h) {
        return (
            <AnchoredHeading level={1}>
                <span>Hello</span> world!
            </AnchoredHeading>
        )
    }
})

h 作為 createElement 的別名是 Vue 生態(tài)系統(tǒng)中的一個(gè)通用慣例,實(shí)際上也是 JSX 所要求的,如果在作用域中 h 失去作用,在應(yīng)用中會(huì)觸發(fā)報(bào)錯(cuò)。

總結(jié)

回過(guò)頭來(lái)看,Vue中的渲染核心關(guān)鍵的幾步流程還是非常清晰的:

  • new Vue,執(zhí)行初始化
  • 掛載$mount方法,通過(guò)自定義render方法、templateel等生成render函數(shù)
  • 通過(guò)Watcher監(jiān)聽數(shù)據(jù)的變化
  • 當(dāng)數(shù)據(jù)發(fā)生變化時(shí),render函數(shù)執(zhí)行生成VNode對(duì)象
  • 通過(guò)patch方法,對(duì)比新舊VNode對(duì)象,通過(guò)DOM Diff算法,添加、修改、刪除真正的DOM元素

至此,整個(gè)new Vue的渲染過(guò)程完畢。

而這篇文章,主要把精力集中在render函數(shù)這一部分。學(xué)習(xí)了怎么用render函數(shù)來(lái)創(chuàng)建組件,以及了解了其中createElement

最后要說(shuō)的是,上文雖然以學(xué)習(xí)render函數(shù),但文中涉及了Vue不少的知識(shí)點(diǎn),也有點(diǎn)零亂。初學(xué)者自己根據(jù)自己獲取所要的知識(shí)點(diǎn)。由于本人也是初涉Vue相關(guān)的知識(shí)點(diǎn),如果文章中有不對(duì)之處,煩請(qǐng)路過(guò)的大神拍正。

此文轉(zhuǎn)載于:[https://www.w3cplus.com/vue/vue-render-function.html]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,125評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,402評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,934評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,168評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,690評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,596評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評(píng)論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,027評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,398評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,743評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容

  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容,還有我對(duì)于 Vue 1.0 印象不深的內(nèi)容。關(guān)于...
    云之外閱讀 5,064評(píng)論 0 29
  • Vue 推薦在絕大多數(shù)情況下使用 template 來(lái)創(chuàng)建你的 HTML。然而在一些場(chǎng)景中,你真的需要 JavaS...
    O8閱讀 2,423評(píng)論 0 3
  • 回憶 首先,render函數(shù)中手寫h=>h(app),new Vue()實(shí)例初始化init()和原來(lái)一樣。$mou...
    LoveBugs_King閱讀 2,290評(píng)論 1 2
  • 01 愛情是什么?再甜美永恒的愛情也長(zhǎng)不過(guò)時(shí)間,躲不過(guò)易變的人心。 那些赤誠(chéng)天真愛過(guò)的人,多半在歇斯底里中,日漸心...
    時(shí)光說(shuō)故事閱讀 740評(píng)論 0 4
  • 2017.8.2 剛裝修好的房子木框架,載著我們整體移動(dòng),將被卡到合適的位置。移動(dòng)過(guò)程中,前面有個(gè)大三輪車擋在小道...
    奕蘅王閱讀 199評(píng)論 0 0