大多時候,我會使用template, vue單文件去渲染組件。雖然知道Vue中有個render函數,但卻不多在項目中去主動使用它。使用最多的地方是在使用一些UI框架的時候,好比iview table中的按鈕操做,會使用到render函數。另外平時在閱讀一些Vue UI框架源碼的時候,也時常能遇到使用render函數的地方,這也激發了本身研究學習的慾望。若是你也感興趣,那就繼續閱讀吧。vue
在本文中,會有以下內容:node
讓咱們開始吧!數組
Vue.js模板功能強大,幾乎能夠知足咱們在應用程序中所需的一切。可是,有一些場景下,好比基於輸入或插槽值建立動態組件,render函數能夠更好地知足這些用例。瀏覽器
那些來自React世界的開發者可能對render函數很是熟悉。一般在JSX
中使用它們來構建React組件。雖然Vue渲染函數也能夠用JSX編寫,但咱們將繼續使用原始JS,有助於咱們能夠更輕鬆地瞭解Vue組件系統的基礎。。babel
每一個Vue組件都實現了一個render函數。大多數時候,該函數將由Vue編譯器建立。當咱們在組件上指定模板時,該模板的內容將由Vue編譯器處理,編譯器最終將返回render函數。渲染函數本質上返回一個虛擬DOM節點,該節點將被Vue在瀏覽器DOM中渲染。app
如今又引出了虛擬DOM的概念,"虛擬DOM究竟是什麼?"框架
虛擬文檔對象模型(或"DOM")容許Vue在更新瀏覽器以前在其內存中渲染組件。這使一切變得更快,同時也避免了DOM從新渲染的高昂成本。由於每一個DOM節點對象包含不少屬性和方法,所以使用虛擬DOM預先在內存進行操做,能夠省去不少瀏覽器直接建立DOM節點對象的開銷。iview
Vue更新瀏覽器DOM時,會將更新的虛擬DOM與上一個虛擬DOM進行比較,並僅使用已修改的部分更新實際DOM。這意味着更少的元素更改,從而提升了性能。Render函數返回虛擬DOM節點,在Vue生態系統中一般稱爲VNode,該接口是容許Vue在瀏覽器DOM中寫入這些對象的接口。它們包含使用Vue所需的全部信息。 Vue的下一版本將包含一個全新的虛擬DOM實現,該實現將比目前更快。 React,Riot,Inferno等許多其餘框架也使用虛擬DOM的概念。dom
咱們能夠在任何Vue組件中實現Vue render函數。一樣,因爲Vue的數據響應性,每當組件的數據獲得更新時,都會再次調用render函數。函數
這是一個簡單的示例,說明如何直接使用組件中的render函數去渲染h1標籤:
new Vue({ el: '#app', render(createElement) { return createElement('h1', 'Hello world'); } });
有一些內置組件能夠利用渲染函數的功能,例如transition和keep-alive。這些組件直接在渲染函數中操縱VNode。若是Vue沒有提供這個函數特性,這些功能將沒法實現。
大多數時候,Vue渲染函數將在項目構建期間由Vue編譯器進行編譯(例如,使用Webpack)。所以,編譯器不會最終出如今您的生產代碼中,從而減少了包的體積。這就是爲何當您使用"單個文件組件"時,除非咱們確實須要/想要,不然您實際上不須要使用render函數。
可是,若是咱們想在代碼中使用編譯器,則可使用帶有編譯器的Vue版本。簡而言之,咱們正在使用Vue編譯器來編譯自定義模板。假設咱們在作一個電商項目,那麼能夠將其注入購物車,從而能夠擁有更多的控制。
咱們編寫了一個實現自定義渲染功能的組件,該功能可獲取用戶建立的模板並替換咱們的默認模板。
這是一個如何使用編譯器將模板字符串編譯爲渲染函數的快速示例:
const template = ` <ul> <li v-for="item in items"> {{ item }} </li> </ul>`; const compiledTemplate = Vue.compile(template); new Vue({ el: '#app', data() { return { items: ['Item1', 'Item2'] } }, render(createElement) { return compiledTemplate.render.call(this, createElement); } });
如您所見,編譯器將返回一個對象(compiledTemplate),其中包含準備使用的render函數。
具備渲染功能的組件沒有模板標記或屬性。相反,他們定義了一個稱爲render的函數,該函數接收一個createElement(renderElement:String | Component,define:Object,children:String | Array)
參數(因爲某種緣由,一般別名爲h,歸咎於JSX)並返回使用該函數建立的元素。其餘一切保持不變。
export default { data() { return { isRed: true } }, /* * 和下邊使用template相同 * <template> * <div :class="{'is-red': isRed}"> * <p>Example Text</p> * </div> * </template> */ render(h) { return h('div', { 'class': { 'is-red': this.isRed } }, [ h('p', 'Example Text') ]) } }
Vue模板具備各類便捷功能,以便向模板添加基本邏輯和綁定功能。渲染函數沒法訪問它們。取而代之的是,它們必須以純Javascript實現,對於大多數指令而言,這是至關簡單的。
這個很簡單。除了使用v-if以外,還能夠在createElement
中調用普通的if(expr)
語句。
<ul v-if="items.length"> <li v-for="item in items">{{ item.name }}</li> </ul> <p v-else>No items found.</p>
這些均可以在渲染函數中用JavaScript的if/else 和 map
來重寫:
props: ['items'], render: function (h) { if (this.items.length) { return h('ul', this.items.map(function (item) { return h('li', item.name) })) } else { return h('p', 'No items found.') } }
v-for可使用for-of,Array.map,Array.filter等多種迭代方法中的任何一種來實現。咱們能夠將它們以很是有趣的方式組合在一塊兒,以實現過濾或分割數據。
例如,咱們能夠替換
<template> <ul> <li v-for="item of list"> </li> </ul> </template>
爲
render(h) { return h('ul', this.list.map(item => h('li', item.name))); }
要記住的一件事是,v-model
就是綁定屬性爲value(還能夠是其餘屬性),只要觸發input事件並設置data屬性的簡寫形式。不幸的是,渲染函數沒有這麼簡寫。咱們必須本身實現它,以下所示。
render(h) { return h('input', { domProps: { value: this.myBoundProperty }, on: { input: e => { this.myBoundProperty = e.target.value } } }) }
等效於:
<template> <input :value="myBoundProperty" @input="myBoundProperty = $event.target.value"/> </template>
或
<template> <input v-model="myBoundProperty"/> </template>
屬性和屬性綁定以attrs,props和domProps
(相似於value和innerHTML之類)的形式放置在元素定義中。
render(h) { return h('div', { attrs: { // <div :id="myCustomId"> id: this.myCustomId }, props: { // <div :someProp="someValue"> someProp: this.someValue }, domProps: { // <div :value="someValue"> value: this.someValue } }); }
附帶說明一下,類和樣式綁定直接在定義的根進行處理,而不是做爲attrs,props或domProps處理。
render(h) { return h('div', { // "class" 是JS中的保留字, 因此須要引號包起來. 'class': { myClass: true, theirClass: false }, style: { backgroundColor: 'green' } }); }
事件處理程序也能夠直接在on
(或nativeOn
,與組件的v-on.native
效果相同)中直接添加到元素定義中。
render(h) { return h('div', { on: { click(e) { console.log('我點擊了!') } } }); }
能夠經過this.$slots
做爲createElement()節點的數組來訪問插槽。
<div id="app"> <myslot> <div slot="slot1">this is slot1</div> <div slot="slot2">This is slot2 </div> </myslot> <script> var app = new Vue({ el: '#app' }) </script>
Vue.component('my-slot', { render: function(h) { const child = h('div', { domProps: {innerHTML: 'this is child'} }); return h( 'div', [ child, this.$slots.slot1, this.$slots.slot2 ] ) } })
做用域插槽存儲在this.$scopedSlots[scope](props:object)
中,做爲返回createElement()節點數組的函數。
<div id="app"> <myslot> <template scope="props"> <div>{{props.text}}</div> </template> </myslot> <script> var app = new Vue({ el: '#app' }) </script>
Vue.component('my-slot', { render: function(h) { const child = h('div', { domProps: {innerHTML: 'this is child'} }); return h( 'div', [ child, this.$scopedSlots.default({ text: 'hello scope' }) ] ) } })
createElement
函數能夠接收稱爲數據對象
的參數。該對象能夠具備多個屬性,這些屬性與咱們在標準模板中使用的v-bind:on
等指令等效。這是帶有按鈕的簡單計數器組件的示例,該按鈕能夠增長點擊次數。
new Vue({ el: '#app', data() { return { clickCount: 0, } }, methods: { onClick() { this.clickCount += 1; } }, render(h) { const button = h('button', { on: { click: this.onClick } }, 'Click me'); const counter = h('span', [ 'Number of clicks:', this.clickCount ]); return h('div', [ button, counter ]) } });
可是數據對象不限於事件綁定!咱們也能夠像使用v-bind:class
指令同樣將CSS類應用於元素。
new Vue({ el: '#app', data() { return { clickCount: 0, } }, computed: { backgroundColor() { return { 'pink': this.clickCount%2 === 0, 'green': this.clickCount%2 !== 0, }; } }, methods: { onClick() { this.clickCount += 1; } }, render(h) { const button = h('button', { on: { click: this.onClick } }, 'Click me'); const counter = h('span', { 'class': this.backgroundColor, }, [ 'Number of clicks:', this.clickCount ]); return h('div', [ button, counter ]) } });
我認爲了解Vue的幕後工做很是有趣。要知道是否可以最有效地使用工具,惟一的方法是確切地瞭解它的工做方式。
這並非說咱們應該開始將全部模板都轉換爲render函數,可是有時它們能夠派上用場,因此咱們至少應該知道如何使用它們。
在上面的示例中,我展現瞭如何在組件中使用自定義render函數,該函數容許咱們的某些組件可重寫。
首先,讓咱們建立初始模板。
<div id="app"> <heading> <span>Heading title is: {{ title }}</span> </heading> </div>
在將要安裝Vue應用程序的div內,咱們編寫一個自定義模板。接下來,咱們但願模板覆蓋咱們將要建立的標題組件的默認版本。
咱們要作的第一件事是遍歷自定義模板,並使用Vue編譯器對其進行預編譯:
const templates = []; const templatesContainer = document.getElementById('app'); // 咱們遍歷每一個自定義模板並對其進行預編譯 for (var i = 0; i < templatesContainer.children.length; i++) { const template = templatesContainer.children.item(i); templates.push({ name: template.nodeName.toLowerCase(), renderFunction: Vue.compile(template.innerHTML), }); }
而後,讓咱們建立標題組件:
Vue.component('heading', { props: ['title'], template: ` <overridable name="heading"> <h1> {{ title }} </h1> </overridable>` });
如今,它只是一個簡單的組件,具備一個名爲title的屬性。默認模板將渲染帶有標題的h1。咱們將用隨後建立的overridable
組件包裝該組件。
這是咱們將使用自定義渲染功能的地方。
Vue.component('overridable', { props: ['name'], render(createElement) { // 咱們使用在初始化應用程序時建立的模板數組 const template = templates.find(x => x.name === this.name); // 當沒有自定義模板時,咱們將使用默認插槽返回默認內容。 if (!template) { return this.$slots.default[0]; } // 使用預編譯的render函數 return template.renderFunction.render.call(this.$parent, createElement); } });
而後,讓咱們掛載Vue應用程序:
new Vue({ el: '#app', template: `<heading title="Hello world"></heading>` });
在此示例中,咱們能夠看到使用了默認模板,所以它是一個標準的h1
標籤。
若是將自定義模板添加到div#app內,則會看到標題組件會被渲染成咱們指定的自定義模板。
若是使用render函數建立組件,讓你感受很是繁瑣。那麼還能夠嘗試babel-plugin-transform-vue-jsx
插件,就能夠像在React中那樣輕便(屬性細節略有不一樣,具體參考插件文檔)。
整體而言,使用render函數很是有趣,而且在v3.0中也派上了用場。 Vue渲染函數是Vue自己的基本組成部分,所以,我真的認爲花一些時間並完全理解該概念(特別是長期使用該框架)頗有價值。隨着Vue.js的發展和效率的提升,咱們平時積累的這些底層基礎知識也有助於咱們的發展。
換句話說,瞭解Vue render函數只是你技術進步中的一小步,但很重要。 :)