Vue render函數

前戲

在瞭解vue render函數以前, 須要先了解下Vue的總體流程(如上圖)javascript

經過上圖, 應該能夠理解一個Vue組件是如何運行起來的.html

  • 模板經過編譯生成AST樹
  • AST樹生成Vue的render渲染函數
  • render渲染函數結合數據生成vNode(Virtual DOM Node)樹
  • Diff和Patch後生新的UI界面(真實DOM渲染)

在這張圖中, 咱們須要瞭解如下幾個概念:vue

  • 模板, Vue模板是純HTML, 基於Vue的模板語法, 能夠比較方便的處理數據和UI界面的關係
  • AST, 即Abstract Syntax Tree的簡稱, Vue將HTML模板解析爲AST,並對AST進行一些優化的標記處理, 提取最大的靜態樹,以使Virtual DOM直接跳事後面的Diff
  • render渲染函數, render渲染函數是用來生成Virtual DOM的. Vue推薦使用模板來構建咱們的應用程序, 在底層實現中Vue最終仍是會將模板編譯成渲染函數. 所以, 若咱們想要獲得更好的控制, 能夠直接寫渲染函數.(重點)
  • Virtual DOM, 虛擬DOM
  • Watcher, 每一個Vue組件都有一個對應的watcher, 它會在組件render時收集組件所依賴的數據, 並在依賴有更新時, 觸發組件從新渲染, Vue會自動優化並更新須要更新DOM

在上圖中, render函數能夠做爲一道分割線:java

  • render函數左邊能夠稱爲編譯期, 將Vue板轉換爲渲染函數
  • render函數右邊, 是Vue運行時, 主要是將渲染函數生成Virtual DOM樹, 以及Diff和Patch

render

Vue 推薦在絕大多數狀況下使用模板來建立你的 HTML。然而在一些場景中,你真的須要 JavaScript 的徹底編程的能力。這時你能夠用渲染函數,它比模板更接近編譯器。node

渲染標題的例子

例如, 官網上一個渲染標題的例子git

相關的實現, 你們能夠查閱下, 這裏再也不細述了. 這裏貼上template的實現和render函數的實現的代碼:算法

.vue單文件的實現

<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>
</template>
<script> export default { name: 'anchored-heading', props: { level: { type: Number, required: true } } } </script>
複製代碼

render函數的實現

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

是否是很簡潔了?編程

Node & tree & Virtual DOM

在對Vue的基礎概念和渲染函數有必定了解後, 咱們也須要了解一些瀏覽器的工做原理. 這對咱們學習render函數很重要. 例以下面這段HTML代碼:segmentfault

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

當瀏覽器讀取到這些代碼時, 它會創建一個DOM節點樹來保持追蹤, 若是你要畫一張家譜樹來追蹤家庭成員的發展的話, HTML的DOM節點樹的可能以下圖所示:瀏覽器

每一個元素文字都是一個節點, 甚至註釋也是節點. 一個節點就是頁面的一部分, 就像家譜樹中同樣, 每一個節點均可以有孩子節點.

高效的更新全部節點多是比較困難的, 不過你不用擔憂, 這些Vue都會自動幫你完成, 你只須要通知Vue頁面上HTML是什麼?

能夠是一個HTML模板, 例如:

<h1>{{title}}</h1>
複製代碼

也能夠是一個渲染函數:

render(h){
  return h('h1', this.title)
}
複製代碼

在這兩種狀況下,若title值發生了改變, Vue 都會自動保持頁面的更新.

虛擬DOM

Vue編譯器在編譯模板以後, 會將這些模板編譯爲渲染函數(render), 當渲染函數(render)被調用時, 就會返回一個虛擬DOM樹.

當咱們獲得虛擬DOM樹後, 再轉交給一個Patch函數, 它會負責把這些虛擬DOM渲染爲真實DOM. 在這個過程當中, Vue自身的響應式系統會偵測在渲染過程當中所依賴的數據來源, 在渲染過程當中, 偵測到數據來源後便可精確感知數據源的變更, 以便在須要的時候從新進行渲染. 當從新進行渲染以後, 會生成一個新的樹, 將新的樹與舊的樹進行對比, 就能夠獲得最終須要對真實DOM進行修改的改動點, 最後經過Patch函數實施改動.

簡單來說, 即: 在Vue的底層實現上,Vue將模板編譯成虛擬DOM渲染函數。結合Vue自帶的響應系統,在應該狀態改變時,Vue可以智能地計算出從新渲染組件的最小代價並應到DOM操做上。

Vue支持咱們經過data參數傳遞一個JavaScript對象做爲組件數據, Vue將遍歷data對象屬性, 使用Object.defineProperty方法設置描述對象, 經過gett/setter函數來攔截對該屬性的讀取和修改.

Vue建立了一層Watcher層, 在組件渲染的過程當中把屬性記錄爲依賴, 當依賴項的setter被調用時, 會通知Watcher從新計算, 從而使它關聯的組件得以更新.

對於虛擬DOM, 若是想深刻了解, 能夠看下Vue原理解析之Virtual DOM

經過前面的學習, 咱們初步瞭解到Vue經過創建一個**虛擬DOM"對真實DOM發生變化保持追蹤. 例如

return createElement('h1', this.title)
複製代碼

createElement, 即createNodeDescription, 返回虛擬節點(Virtual Node), 一般簡寫爲"VNode". 虛擬DOM是由Vue組件樹創建起來的整個VNode樹的總稱.

Vue組件樹創建起來的整個VNode樹是惟一的, 不可重複的. 例如, 下面的render函數是無效的.

render(createElement) {
  const vP = createElement('p', 'hello james')
  return createElement('div', [
    // error, 有重複的vNode
    vP, vP
  ])
}
複製代碼

若須要不少重複的組件/元素, 可使用工廠函數來實現. 例如:

render(createElement){
  return createElement('div', Array.apply(null, {length: 20}).map(() => {
    return createElement('p', 'hi james')
  }))
}
複製代碼

Vue 渲染機制

下圖展現的是獨立構建時, 一個組件的渲染流程圖:

會涉及到Vue的2個概念:

  • 獨立構建, 包含模板編譯器, 渲染過程: HTML字符串 => render函數 => vNode => 真實DOM
  • 運行時構建, 不包含模板編譯器, 渲染過程: render函數 => vNode => 真實DOM

運行時構建的包, 會比獨立構建少一個模板編譯器(所以運行速度上會更快). 在$mount函數上也不一樣, 而$mount方法是整個渲染過程當中的起始點, 用下面這張流程圖來講明:

從上圖能夠看出, 在渲染過程當中, 提供了三種模板:

  • 自定義render函數
  • template
  • el

都可以渲染頁面, 也就對應咱們使用Vue時的三種寫法. 這3種模式最終都是要獲得render函數.

對於平時開發來說, 使用template和el會比較友好些, 容易理解, 但靈活性較差. 而render函數, 可以勝任更加複雜的邏輯, 靈活性高, 但對於用戶理解相對較差.

自定義render函數

Vue.component('anchored-heading', {
    render(createElement) {
        return createElement (
            'h' + this.level,   
            this.$slots.default 
        )
    },
    props: {
        level: {
            type: Number,
            required: true
        }
    }
})
複製代碼

template寫法

const app = new Vue({
    template: `<div>{{ msg }}</div>`,
    data () {
        return {
            msg: 'Hello Vue.js!'
        }
    }
})
複製代碼

el寫法

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

理解&使用render函數

createElement

在使用render函數時, createElement是必需要掌握的.

createElement 參數

createElement能夠接受多個參數

第1個參數: {String | Object | Function }, 必傳

第一個參數是必傳參數, 能夠是字符串String, 也能夠是Object對象或函數Function

// String
Vue.component('custom-element', {
    render(createElement) {
        return createElement('div', 'hello world!')
    }
})
// Object
Vue.component('custom-element', {
    render(createElement) {
        return createElement({
          template: `<div>hello world!</div>`
        })
    }
})
// Function
Vue.component('custom-element', {
    render(createElement) {
      const elFn = () => { template: `<div>hello world!</div>` }
      return createElement(elFn())
    }
})
複製代碼

以上代碼, 等價於:

<template>
  <div>hello world!</>
</template>
<script> export default { name: 'custom-element' } </script>
複製代碼

第2個參數: { Object }, 可選

createElemen的第二個參數是可選參數, 這個參數是一個Object, 例如:

Vue.component('custom-element', {
  render(createElement) {
    const self = this;
    return createElement('div', {
      'class': {
        foo: true,
        bar: false
      },
      style: {
        color: 'red',
        fontSize: '18px'
      },
      attrs: {
        ...self.attrs,
        id: 'id-demo'
      },
      on: {
        ...self.$listeners,
        click: (e) => {console.log(e)}
      },
      domProps: {
        innerHTML: 'hello world!'
      },
      staticClass: 'wrapper'
    })
  }
})
複製代碼

等價於:

<template>
  <div :id="id" class="wrapper" :class="{'foo': true, 'bar': false}" :style="{color: 'red', fontSize: '18px'}" v-bind="$attrs" v-on="$listeners" @click="(e) => console.log(e)"> hello world! </div>
</template>
<script> export default { name: 'custom-element', data(){ return { id: 'id-demo' } } } </script>

<style> .wrapper{ display: block; width: 100%; } </style>
複製代碼

第3個參數: { String | Array }, 可選

createElement第3個參數是可選的,能夠給其傳一個StringArray, 例如:

Vue.component('custom-element', {
    render (createElement) {
        var self = this
        return createElement(
            'div',
            {
                class: {
                    title: true
                },
                style: {
                    border: '1px solid',
                    padding: '10px'
                }
            }, 
            [
                createElement('h1', 'Hello Vue!'),
                createElement('p', 'Hello world!')
            ]
        )
    }
})
複製代碼

等價於:

<template>
  <div :class="{'title': true}" :style="{border: '1px solid', padding: '10px'}">
    <h1>Hello Vue!</h1>
    <p>Hello world!</p>
  </div>
</template>
<script> export default { name: 'custom-element', data(){ return { id: 'id-demo' } } } </script>
複製代碼

使用template和render建立相同效果的組件

template方式

<template>
  <div id="wrapper" :class="{show: show}" @click="clickHandler">
    Hello Vue!
  </div>
</template>
<script> export default { name: 'custom-element', data(){ return { show: true } }, methods: { clickHandler(){ console.log('you had click me!'); } } } </script>
複製代碼

render方式

Vue.component('custom-element', {
      data () {
        return {
            show: true
        }
      },
      methods: {
          clickHandler: function(){
            console.log('you had click me!');
          }
      },
      render: function (createElement) {
          return createElement('div', {
              class: {
                show: this.show
              },
              attrs: {
                id: 'wrapper'
              },
              on: {
                click: this.handleClick
              }
          }, 'Hello Vue!')
      }
})
複製代碼

createElement解析過程

createElement解析流程圖(摘至: segmentfault.com/a/119000000…)

createElement解析過程核心源代碼(須要對JS有必定功底, 摘至: segmentfault.com/a/119000000…)

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應該設置爲常量ALWAYS_NORMALIZE的值
    if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
        // 調用_createElement建立虛擬節點
        return _createElement(context, tag, data, children, normalizationType)
    }

    function _createElement (context, tag, data, children, normalizationType) {
        /** * 若是存在data.__ob__,說明data是被Observer觀察的數據 * 不能用做虛擬節點的data * 須要拋出警告,並返回一個空節點 * * 被監控的data不能被用做vnode渲染的數據的緣由是: * data在vnode渲染過程當中可能會被改變,這樣會觸發監控,致使不符合預期的操做 */
        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()
        }

        // 當組件的is屬性被設置爲一個falsy的值
        // Vue將不會知道要把這個組件渲染成什麼
        // 因此渲染一個空節點
        if (!tag) {
            return createEmptyVNode()
        }

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

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

        // 若是標籤名是字符串類型
        if (typeof tag === 'string') {
            let Ctor
            // 獲取標籤名的命名空間
            ns = config.getTagNamespace(tag)

            // 判斷是否爲保留標籤
            if (config.isReservedTag(tag)) {
                // 若是是保留標籤,就建立一個這樣的vnode
                vnode = new VNode(
                    config.parsePlatformTagName(tag), data, children,
                    undefined, undefined, context
                )

                // 若是不是保留標籤,那麼咱們將嘗試從vm的components上查找是否有這個標籤的定義
            } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
                // 若是找到了這個標籤的定義,就以此建立虛擬組件節點
                vnode = createComponent(Ctor, data, context, children, tag)
            } else {
                // 兜底方案,正常建立一個vnode
                vnode = new VNode(
                    tag, data, children,
                    undefined, undefined, context
                )
            }

        // 當tag不是字符串的時候,咱們認爲tag是組件的構造類
        // 因此直接建立
        } else {
            vnode = createComponent(tag, data, context, children)
        }

        // 若是有vnode
        if (vnode) {
            // 若是有namespace,就應用下namespace,而後返回vnode
            if (ns) applyNS(vnode, ns)
            return vnode
        // 不然,返回一個空節點
        } else {
            return createEmptyVNode()
        }
    }
}

複製代碼

使用render函數代替模板功能

在使用Vue模板的時候,咱們能夠在模板中靈活的使用v-ifv-forv-model<slot>等模板語法。但在render函數中是沒有提供專用的API。若是在render使用這些,須要使用原生的JavaScript來實現。

v-if & v-for

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

render函數實現

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

複製代碼

v-model

<template>
  <el-input :name="name" @input="val => name = val"></el-input>
</template>
<script> export default { name: 'app', data(){ return { name: 'hello vue.js' } } } </script>
複製代碼

render函數實現

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

複製代碼

slot

在Vue中, 能夠經過:

  • this.$slots獲取VNodes列表中的靜態內容.
render(h){
  return h('div', this.$slots.default)
}
複製代碼

等價於:

<template> 
  <div>
    <slot> </slot>
  </div>
</template>
複製代碼

在Vue中, 能夠經過:

  • this.$scopedSlots獲取能用做函數的做用域插槽, 這個函數會返回VNodes
props: ['message'],
render (createElement) {
    // `<div><slot :text="message"></slot></div>`
    return createElement('div', [
        this.$scopedSlots.default({
            text: this.message
        })
    ])
}
複製代碼

若是要用渲染函數向子組件中傳遞做用域插槽,能夠利用VNode數據中的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

若是寫習慣了template,而後要用render函數來寫,必定會感受狠難受,特別是面對複雜的組件的時候。不過咱們在Vue中使用JSX可讓咱們回到更接近於模板的語法上。

import View from './View.vue'

new Vue({
    el: '#demo',
    render (h) {
        return (
            <View level={1}> <span>Hello</span> world! </View>
        )
    }
}
複製代碼

將 h 做爲 createElement 的別名是 Vue 生態系統中的一個通用慣例,實際上也是 JSX 所要求的,若是在做用域中 h 失去做用,在應用中會觸發報錯。

總結

Vue渲染中, 核心關鍵的幾步是:

  • new Vue, 執行初始化
  • 掛載$mount, 經過自定義render方法, template, el等生成render渲染函數
  • 經過Watcher監聽數據的變化
  • 當數據發生變化時, render函數執行生成VNode對象
  • 經過patch方法, 對比新舊VNode對象, 經過DOM Diff算法, 添加/修改/刪除真正的DOM元素

至此, 整個new Vue渲染過程完成.

相關連接

相關文章
相關標籤/搜索