在瞭解vue render函數以前, 須要先了解下Vue的總體流程(如上圖)javascript
經過上圖, 應該能夠理解一個Vue組件是如何運行起來的.html
在這張圖中, 咱們須要瞭解如下幾個概念:vue
watcher
, 它會在組件render
時收集組件所依賴的數據, 並在依賴有更新時, 觸發組件從新渲染, Vue會自動優化並更新須要更新DOM在上圖中, render
函數能夠做爲一道分割線:java
render
函數左邊能夠稱爲編譯期, 將Vue板轉換爲渲染函數render
函數右邊, 是Vue運行時, 主要是將渲染函數生成Virtual DOM樹, 以及Diff和PatchVue 推薦在絕大多數狀況下使用模板來建立你的 HTML。然而在一些場景中,你真的須要 JavaScript 的徹底編程的能力。這時你能夠用渲染函數,它比模板更接近編譯器。node
例如, 官網上一個渲染標題的例子git
相關的實現, 你們能夠查閱下, 這裏再也不細述了. 這裏貼上template的實現和render函數的實現的代碼:算法
<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> 複製代碼
Vue.component('anchored-heading', { render: function (createElement) { return createElement( 'h' + this.level, // tag name 標籤名稱 this.$slots.default // 子組件中的陣列 ) }, props: { level: { type: Number, required: true } } }) 複製代碼
是否是很簡潔了?編程
在對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 都會自動保持頁面的更新.
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的2個概念:
運行時構建的包, 會比獨立構建少一個模板編譯器(所以運行速度上會更快). 在$mount
函數上也不一樣, 而$mount
方法是整個渲染過程當中的起始點, 用下面這張流程圖來講明:
從上圖能夠看出, 在渲染過程當中, 提供了三種模板:
都可以渲染頁面, 也就對應咱們使用Vue時的三種寫法. 這3種模式最終都是要獲得render
函數.
對於平時開發來說, 使用template和el會比較友好些, 容易理解, 但靈活性較差. 而render函數, 可以勝任更加複雜的邏輯, 靈活性高, 但對於用戶理解相對較差.
Vue.component('anchored-heading', { render(createElement) { return createElement ( 'h' + this.level, this.$slots.default ) }, props: { level: { type: Number, required: true } } }) 複製代碼
const app = new Vue({ template: `<div>{{ msg }}</div>`, data () { return { msg: 'Hello Vue.js!' } } }) 複製代碼
let app = new Vue({ el: '#app', data () { return { msg: 'Hello Vue!' } } }) 複製代碼
在使用render
函數時, createElement
是必需要掌握的.
createElement
能夠接受多個參數
{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> 複製代碼
{ 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> 複製代碼
{ String | Array }
, 可選createElement
第3個參數是可選的,能夠給其傳一個String
或Array
, 例如:
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方式
<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解析流程圖(摘至: 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() } } } 複製代碼
在使用Vue模板的時候,咱們能夠在模板中靈活的使用v-if
、v-for
、v-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 } }) 複製代碼
在Vue中, 能夠經過:
this.$slots
獲取VNodes列表中的靜態內容.render(h){ return h('div', this.$slots.default) } 複製代碼
等價於:
<template> <div> <slot> </slot> </div> </template> 複製代碼
在Vue中, 能夠經過:
this.$scopedSlots
獲取能用做函數的做用域插槽, 這個函數會返回VNodesprops: ['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' } 複製代碼
若是寫習慣了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
渲染過程完成.