理解Vue中的Render渲染函式
VUE一般使用template來建立HTML,然後在有的時候,我們需要使用javascript來建立html,這時候我們需要使用render函式。
比如如下我想要實現如下html:
<div id="container"> <h1> <a href="#"> Hello world! </a> </h1> </div>
我們會如下使用:
<!DOCTYPE html> <html> <head> <title>演示Vue</title> <style> </style> </head> <body> <div id="container"> <tb-heading :level="1"> <a href="#">Hello world!</a> </tb-heading> </div> </body> <script src="./vue.js"></script> <script type="text/x-template" id="templateId"> <h1 v-if="level === 1"> <slot></slot> </h1> <h2 v-else-if="level === 2"> <slot></slot> </h2> </script> <script> Vue.component('tb-heading', { template: '#templateId', props: { level: { type: Number, required: true } } }); new Vue({ el: '#container' }); </script> </html>
如上程式碼是根據引數 :level來顯示不同級別的標題中插入錨點元素,我們需要重複的使用 <slot></slot>.
下面我們來嘗試使用 render函式重寫上面的demo;如下程式碼:
<!DOCTYPE html> <html> <head> <title>演示Vue</title> <style> </style> </head> <body> <div id="container"> <tb-heading :level="2"> <a href="#">Hello world!</a> </tb-heading> </div> </body> <script src="./vue.js"></script> <script> Vue.component('tb-heading', { render: function(createElement) { return createElement( 'h' + this.level, // tag name 標籤名稱 this.$slots.default // 元件的子元素 ) }, props: { level: { type: Number, required: true } } }); new Vue({ el: '#container' }); </script> </html>
如上 render函式程式碼看起來非常簡單就實現了,元件中的子元素儲存在元件實列中 $slots.default 中。
理解createElement
Vue通過建立一個虛擬DOM對真實的DOM發生變化儲存追蹤,如下程式碼:
return createElement('h1', this.title);
createElement返回的是包含的資訊會告訴VUE頁面上需要渲染什麼樣的節點及其子節點。我們稱這樣的節點為虛擬DOM,可以簡寫為VNode,
createElement 引數 // @return {VNode} createElement( // {String | Object | Function} // 一個HTML標籤字串,元件選項物件,或者一個返回值型別為String/Object的函式。該引數是必須的 'div', // {Object} // 一個包含模板相關屬性的資料物件,這樣我們可以在template中使用這些屬性,該引數是可選的。 { }, // {String | Array} // 子節點(VNodes)由 createElement() 構建而成。可選引數 // 或簡單的使用字串來生成的 "文字節點"。 [ 'xxxx', createElement('h1', '一則頭條'), createElement(MyComponent, { props: { someProp: 'xxx' } }) ] )
理解深入data物件。
在模板語法中,我們可以使用 v-bind:class 和 v-bind:style 來繫結屬性,在VNode資料物件中,下面的屬性名的欄位級別是最高的。
該物件允許我們繫結普通的html特性,就像DOM屬性一樣。如下:
{ // 和`v-bind:class`一樣的 API 'class': { foo: true, bar: false }, // 和`v-bind:style`一樣的 API style: { color: 'red', fontSize: '14px' }, // 正常的 HTML 特性 attrs: { id: 'foo' }, // 元件 props props: { myProp: 'bar' }, // DOM 屬性 domProps: { innerHTML: 'baz' }, // 事件監聽器基於 `on` // 所以不再支援如 `v-on:keyup.enter` 修飾器 // 需要手動匹配 keyCode。 on: { click: this.clickHandler }, // 僅對於元件,用於監聽原生事件,而不是元件內部使用 `vm.$emit` 觸發的事件。 nativeOn: { click: this.nativeClickHandler }, // 自定義指令。注意事項:不能對繫結的舊值設值 // Vue 會為您持續追蹤 directives: [ { name: 'my-custom-directive', value: '2', expression: '1 + 1', arg: 'foo', modifiers: { bar: true } } ], // Scoped slots in the form of // { name: props => VNode | Array<VNode> } scopedSlots: { default: props => createElement('span', props.text) }, // 如果元件是其他元件的子元件,需為插槽指定名稱 slot: 'name-of-slot', // 其他特殊頂層屬性 key: 'myKey', ref: 'myRef' }
上面的data資料可能不太好理解,我們來看一個demo,就知道它是如何使用的了,如下程式碼:
<!DOCTYPE html> <html> <head> <title>演示Vue</title> <style> </style> </head> <body> <div id="container"> <tb-heading :level="2"> Hello world! </tb-heading> </div> </body> <script src="./vue.js"></script> <script> var getChildrenTextContent = function(children) { return children.map(function(node) { return node.children ? getChildrenTextContent(node.children) : node.text }).join('') }; Vue.component('tb-heading', { render: function(createElement) { var headingId = getChildrenTextContent(this.$slots.default) .toLowerCase() .replace(/\W+/g, '-') .replace(/(^\-|\-$)/g, '') return createElement( 'h' + this.level, [ createElement('a', { attrs: { name: headingId, href: '#' + headingId }, style: { color: 'red', fontSize: '20px' }, 'class': { foo: true, bar: false }, // DOM屬性 domProps: { innerHTML: 'baz' }, // 元件props props: { myProp: 'bar' }, // 事件監聽基於 'on' // 所以不再支援如 'v-on:keyup.enter' 修飾語 // 需要手動匹配 KeyCode on: { click: function(event) { event.preventDefault(); console.log(111); } } }, this.$slots.default) ] ) }, props: { level: { type: Number, required: true } } }); new Vue({ el: '#container' }); </script> </html>
對應的屬性使用方法和上面一樣既可以了,我們可以開啟頁面檢視下效果也是可以的。如下
VNodes 不一定必須唯一 (文件中說要唯一)
文件中說 VNode必須唯一;說 下面的 render function 是無效的:
但是我通過測試時可以的,如下程式碼:
<!DOCTYPE html> <html> <head> <title>演示Vue</title> <style> </style> </head> <body> <div id="container"> <tb-heading :level="2"> Hello world! </tb-heading> </div> </body> <script src="./vue.js"></script> <script> Vue.component('tb-heading', { render: function(createElement) { var pElem = createElement('p', 'hello world'); return createElement('div', [ pElem, pElem ]) }, props: { level: { type: Number, required: true } } }); new Vue({ el: '#container' }); </script> </html>
使用Javascript代替模板功能
v-if 和 v-for
template 中有 v-if 和 v-for, 但是vue中的render函式沒有提供專用的API。
比如如下:
<ul v-if="items.length"> <li v-for="item in items">{{ item.name }}</li> </ul> <p v-else>No item found.</p>
在render函式中會被javascript的 if/else 和map重新實現。如下程式碼:
<!DOCTYPE html> <html> <head> <title>演示Vue</title> <style> </style> </head> <body> <div id="container"> <tb-heading> Hello world! </tb-heading> </div> </body> <script src="./vue.js"></script> <script> Vue.component('tb-heading', { render: function(createElement) { console.log(this) if (this.items.length) { return createElement('ul', this.items.map(function(item){ return createElement('li', item.name); })) } else { return createElement('p', 'No items found.'); } }, props: { items: { type: Array, default: function() { return [ { name: 'kongzhi1' }, { name: 'kongzhi2' } ] } } } }); new Vue({ el: '#container' }); </script> </html>
v-model
render函式中沒有 與 v-model相應的api,我們必須自己來實現相應的邏輯。如下程式碼可以實現:
<!DOCTYPE html> <html> <head> <title>演示Vue</title> <style> </style> </head> <body> <div id="container"> <tb-heading @input="inputFunc"> Hello world! </tb-heading> </div> </body> <script src="./vue.js"></script> <script> Vue.component('tb-heading', { render: function(createElement) { var self = this; return createElement('input', { domProps: { value: '11' }, on: { input: function(event) { self.value = event.target.value; self.$emit('input', self.value); } } }) }, props: { } }); new Vue({ el: '#container', methods: { inputFunc: function(value) { console.log(value) } } }); </script> </html>
理解插槽
可以從 this.$slots 獲取VNodes列表中的靜態內容:如下程式碼:
<!DOCTYPE html> <html> <head> <title>演示Vue</title> <style> </style> </head> <body> <div id="container"> <tb-heading :level="2"> <a href="#">Hello world!</a> </tb-heading> </div> </body> <script src="./vue.js"></script> <script> Vue.component('tb-heading', { render: function(createElement) { return createElement( 'h' + this.level, // tag name 標籤名稱 this.$slots.default // 子元件 ) }, props: { level: { type: Number, required: true } } }); new Vue({ el: '#container' }); </script> </html>
理解函式式元件
函式式元件我們標記元件為 functional, 意味著它無狀態(沒有data), 無實列(沒有this上下文)。
一個函式式元件像下面這樣的:
Vue.component('my-component', { functional: true, // 為了彌補缺少的實列 // 提供第二個引數作為上下文 render: function(createElement, context) { }, // Props 可選 props: { } })
元件需要的一切通過上下文傳遞,包括如下:
props: 提供props物件
children: VNode子節點的陣列
slots: slots物件
data: 傳遞給元件的data物件
parent: 對父元件的引用
listeners: (2.3.0+) 一個包含了元件上所註冊的 v-on 偵聽器的物件。這只是一個指向 data.on 的別名。
injections: (2.3.0+) 如果使用了 inject 選項,則該物件包含了應當被注入的屬性。
在新增 functional: true 之後,元件的 render 函式之間簡單更新增加 context 引數,this.$slots.default 更新為 context.children,之後this.level 更新為 context.props.level。
如下程式碼演示:
<!DOCTYPE html> <html> <head> <title>演示Vue</title> <style> </style> </head> <body> <div id="container"> {{msg}} <choice> <item value="1">test</item> </choice> </div> </body> <script src="./vue.js"></script> <script> Vue.component('choice', { template: '<div><ul><slot></slot></ul></div>' }); Vue.component('item', { functional: true, render: function(h, context) { return h('li', { on: { click: function() { console.log(context); console.log(context.parent); console.log(context.props) } } }, context.children) }, props: ['value'] }) new Vue({ el: '#container', data: { msg: 'hello' } }); </script> </html>