Vue 推薦在絕大多數狀況下使用 template 來建立HTML。然而在一些場景中,真的須要 JavaScript 的徹底編程的能力,這就是 render 函數,它比 template 更接近編譯器。本文將詳細介紹Vue渲染函數javascript
下面是一個例子,若是要實現相似下面的效果。其中,H標籤可替換html
<h1> <a name="hello-world" href="#hello-world"> Hello world! </a> </h1>
在 HTML 層,像下面這樣定義來組件接口:vue
<anchored-heading :level="1">Hello world!</anchored-heading>
當開始寫一個經過 level
prop 動態生成 heading 標籤的組件,可能很快想到這樣實現:java
<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>
JS代碼以下node
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
在這種場景中使用 template 並非最好的選擇:首先代碼冗長,爲了在不一樣級別的標題中插入錨點元素,須要重複地使用 <slot></slot>
git
雖然模板在大多數組件中都很是好用,可是在這裏它就不是很簡潔的了。那麼,來嘗試使用 render
函數重寫上面的例子:github
<div id="example"> <anchored-heading :level="2"><a name="hello-world" href="#hello-world">Hello world!</a></anchored-heading> </div> <script src="vue.js"></script> <script> Vue.component('anchored-heading', { render: function (createElement) { return createElement( 'h' + this.level, // tag name 標籤名稱 this.$slots.default // 子組件中的陣列 ) }, props: { level: { type: Number, required: true } } }) new Vue({ el: '#example' }) </script>
這樣的代碼精簡不少,可是須要很是熟悉 Vue 的實例屬性。在這個例子中,須要知道當不使用 slot
屬性向組件中傳遞內容時,好比 anchored-heading
中的 Hello world!
,這些子元素被存儲在組件實例中的 $slots.default
中express
在深刻渲染函數以前,瞭解一些瀏覽器的工做原理是很重要的。如下面這段 HTML 爲例:編程
<div> <h1>My title</h1> Some text content <!-- TODO: Add tagline --> </div>
當瀏覽器讀到這些代碼時,它會創建一個「DOM 節點」樹來保持追蹤,如同會畫一張家譜樹來追蹤家庭成員的發展同樣。HTML 的 DOM 節點樹以下圖所示:api
每一個元素都是一個節點。每段文字也是一個節點。甚至註釋也都是節點。一個節點就是頁面的一個部分。就像家譜樹同樣,每一個節點均可以有子節點 (也就是說每一個部分能夠包含其它的一些部分)
高效的更新全部這些節點會是比較困難的,不過所幸沒必要再手動完成這個工做了。只須要告訴 Vue 但願頁面上的 HTML 是什麼,這能夠是在一個模板裏:
<h1>{{ blogTitle }}</h1>
或者一個渲染函數裏:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
在這兩種狀況下,Vue 都會自動保持頁面的更新,即使 blogTitle
發生了改變。
【虛擬DOM】
Vue 經過創建一個虛擬 DOM 對真實 DOM 發生的變化保持追蹤
return createElement('h1', this.blogTitle)
createElement
到底會返回什麼呢?其實不是一個實際的 DOM 元素。它更準確的名字多是 createNodeDescription
,由於它所包含的信息會告訴 Vue 頁面上須要渲染什麼樣的節點,及其子節點。咱們把這樣的節點描述爲「虛擬節點 (Virtual DOM)」,也常簡寫它爲「VNode」。「虛擬 DOM」是咱們對由 Vue 組件樹創建起來的整個 VNode 樹的稱呼
接下來須要熟悉的是如何在 createElement
函數中生成模板。這裏是 createElement
接受的參數:
// @returns {VNode} createElement( // {String | Object | Function} // 一個 HTML 標籤字符串,組件選項對象,或者一個返回值類型爲 String/Object 的函數,必要參數 'div', // {Object} // 一個包含模板相關屬性的數據對象 // 這樣,能夠在 template 中使用這些屬性。可選參數。 { }, // {String | Array} // 子節點 (VNodes),由 `createElement()` 構建而成, // 或簡單的使用字符串來生成「文本節點」。可選參數。 [ '先寫一些文字', createElement('h1', '一則頭條'), createElement(MyComponent, { props: { someProp: 'foobar' } }) ] )
【深刻data對象】
正如在模板語法中,v-bind:class
和 v-bind:style
,會被特別對待同樣,在 VNode 數據對象中,下列屬性名是級別最高的字段。該對象也容許綁定普通的 HTML 特性,就像 DOM 屬性同樣,好比 innerHTML
(這會取代 v-html
指令)
{ // 和`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' }
【完整示例】
有了這些知識,如今能夠完成最開始想實現的組件:
var getChildrenTextContent = function (children) { return children.map(function (node) { return node.children ? getChildrenTextContent(node.children) : node.text }).join('') } Vue.component('anchored-heading', { render: function (createElement) { // create kebabCase id var headingId = getChildrenTextContent(this.$slots.default) .toLowerCase() .replace(/\W+/g, '-') .replace(/(^\-|\-$)/g, '') return createElement( 'h' + this.level, [ createElement('a', { attrs: { name: headingId, href: '#' + headingId } }, this.$slots.default) ] ) }, props: { level: { type: Number, required: true } } })
【約束】
組件樹中的全部 VNodes 必須是惟一的。這意味着,下面的 render function 是無效的:
render: function (createElement) { var myParagraphVNode = createElement('p', 'hi') return createElement('div', [ // 錯誤-重複的 VNodes myParagraphVNode, myParagraphVNode ]) }
若是真的須要重複不少次的元素/組件,可使用工廠函數來實現。例如,下面這個例子 render 函數完美有效地渲染了 20 個重複的段落:
render: function (createElement) { return createElement('div', Array.apply(null, { length: 20 }).map(function () { return createElement('p', 'hi') }) ) }
【v-if和v-for】
因爲使用原生的 JavaScript 來實現某些東西很簡單,Vue 的 render 函數沒有提供專用的 API。好比,template 中的 v-if
和 v-for
:
<ul v-if="items.length"> <li v-for="item in items">{{ item.name }}</li> </ul> <p v-else>No items found.</p>
這些都會在 render 函數中被 JavaScript 的 if
/else
和 map
重寫:
render: function (createElement) { if (this.items.length) { return createElement('ul', this.items.map(function (item) { return createElement('li', item.name) })) } else { return createElement('p', 'No items found.') } }
【v-model】
render 函數中沒有與 v-model
相應的 api,必須本身來實現相應的邏輯:
render: function (createElement) { var self = this return createElement('input', { domProps: { value: self.value }, on: { input: function (event) { self.value = event.target.value self.$emit('input', event.target.value) } } }) }
這就是深刻底層要付出的,儘管麻煩了一些,但相對於 v-model
來講,能夠更靈活地控制
【事件&按鍵修飾符】
對於 .passive
、.capture
和 .once
事件修飾符,Vue 提供了相應的前綴能夠用於 on
:
Modifier(s) Prefix .passive & .capture ! .once ~ .capture.once or .once.capture ~!
下面是一個例子
on: { '!click': this.doThisInCapturingMode, '~keyup': this.doThisOnce, `~!mouseover`: this.doThisOnceInCapturingMode }
對於其餘的修飾符,前綴不是很重要,由於能夠直接在事件處理函數中使用事件方法:
Modifier(s) Equivalent in Handler .stop event.stopPropagation() .prevent event.preventDefault() .self if (event.target !== event.currentTarget) return Keys: .enter, .13 if (event.keyCode !== 13) return (...) Modifiers Keys: .ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (...)
下面是一個使用全部修飾符的例子:
on: { keyup: function (event) { // 若是觸發事件的元素不是事件綁定的元素 // 則返回 if (event.target !== event.currentTarget) return // 若是按下去的不是 enter 鍵或者 // 沒有同時按下 shift 鍵 // 則返回 if (!event.shiftKey || event.keyCode !== 13) return // 阻止 事件冒泡 event.stopPropagation() // 阻止該元素默認的 keyup 事件 event.preventDefault() // ... } }
【插槽】
能夠從 this.$slots
獲取 VNodes 列表中的靜態內容:
render: function (createElement) { // `<div><slot></slot></div>` return createElement('div', this.$slots.default) }
還能夠從 this.$scopedSlots
中得到能用做函數的做用域插槽,這個函數返回 VNodes:
render: function (createElement) { // `<div><slot :text="msg"></slot></div>` return createElement('div', [ this.$scopedSlots.default({ text: this.msg }) ]) }
若是要用渲染函數向子組件中傳遞做用域插槽,能夠利用 VNode 數據中的 scopedSlots
域:
render (createElement) { return createElement('div', [ createElement('child', { // pass `scopedSlots` in the data object // in the form of { name: props => VNode | Array<VNode> } scopedSlots: { default: function (props) { return createElement('span', props.text) } } }) ]) }
若是寫了不少 render
函數,可能會以爲痛苦
createElement( 'anchored-heading', { props: { level: 1 } }, [ createElement('span', 'Hello'), ' world!' ] )
特別是模板如此簡單的狀況下:
<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>
這就是爲何會有一個 Babel 插件,用於在 Vue 中使用 JSX 語法的緣由,它可讓咱們回到更接近於模板的語法上
import AnchoredHeading from './AnchoredHeading.vue' new Vue({ el: '#demo', render (h) { return ( <AnchoredHeading level={1}> <span>Hello</span> world! </AnchoredHeading> ) } })
[注意]將 h
做爲 createElement
的別名是 Vue 生態系統中的一個通用慣例,實際上也是 JSX 所要求的,若是在做用域中 h
失去做用,在應用中會觸發報錯
以前建立的錨點標題組件是比較簡單,沒有管理或者監放任何傳遞給它的狀態,也沒有生命週期方法。它只是一個接收參數的函數。在這個例子中,咱們標記組件爲 functional
,這意味它是無狀態 (沒有 data
),無實例 (沒有 this
上下文)
一個 函數式組件 就像這樣:
Vue.component('my-component', { functional: true, // 爲了彌補缺乏的實例 // 提供第二個參數做爲上下文 render: function (createElement, context) { // ... }, // Props 可選 props: { // ... } })
[注意]在 2.3.0 以前的版本中,若是一個函數式組件想要接受 props,則 props
選項是必須的。在 2.3.0 或以上的版本中,你能夠省略 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
。
由於函數式組件只是一個函數,因此渲染開銷也低不少。然而,對持久化實例的缺少也意味着函數式組件不會出如今 Vue devtools 的組件樹裏。
在做爲包裝組件時它們也一樣很是有用,好比,當須要作這些時:
一、程序化地在多個組件中選擇一個
二、在將 children, props, data 傳遞給子組件以前操做它們
下面是一個依賴傳入 props 的值的 smart-list
組件例子,它能表明更多具體的組件:
var EmptyList = { /* ... */ } var TableList = { /* ... */ } var OrderedList = { /* ... */ } var UnorderedList = { /* ... */ } Vue.component('smart-list', { functional: true, render: function (createElement, context) { function appropriateListComponent () { var items = context.props.items if (items.length === 0) return EmptyList if (typeof items[0] === 'object') return TableList if (context.props.isOrdered) return OrderedList return UnorderedList } return createElement( appropriateListComponent(), context.data, context.children ) }, props: { items: { type: Array, required: true }, isOrdered: Boolean } })
【slots()和children對比】
爲何同時須要 slots()
和 children
。slots().default
不是和 children
相似的嗎?在一些場景中,是這樣,可是若是是函數式組件和下面這樣的 children 呢?
<my-functional-component>
<p slot="foo">
first
</p>
<p>second</p>
</my-functional-component>
對於這個組件,children
會給兩個段落標籤,而 slots().default
只會傳遞第二個匿名段落標籤,slots().foo
會傳遞第一個具名段落標籤。同時擁有 children
和 slots()
,所以能夠選擇讓組件經過 slot()
系統分發或者簡單的經過 children
接收,讓其餘組件去處理
Vue 的模板實際是編譯成了 render 函數。這是一個實現細節,一般不須要關心。下面是一個使用 Vue.compile
來實時編譯模板字符串的簡單 demo:
<div>
<header>
<h1>I'm a template!</h1>
</header>
<p v-if="message">
{{ message }}
</p>
<p v-else>
No message.
</p>
</div>
render:
function anonymous( ) { with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])} }
staticRenderFns:
_m(0): function anonymous( ) { with(this){return _c('header',[_c('h1',[_v("I'm a template!")])])} }