Vue 推薦在絕大多數情況下使用模板來創建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力。這時你可以用**渲染函數**,它比模板更接近編譯器。
> 在組件選項中有一個渲染函數`render 函數`,而`createElement`是`render 函數`的參數,它本身也是個函數,并且有三個參數。
## **一:createElement()**
>[success] **createElement()函數用于創建虛擬DOM元素**
第一個參數:**HTML標簽名`**或者**`一個組件對象`**或者**`一個 async 函數 。必填項**
第二個參數:一個與模板中屬性對應的`數據對象`。可選。
第三個參數:子元素虛擬節點 (VNodes),由 `createElement()` 構建而成, 也可以使用`字符串`來生成`文本虛擬節點`。可選
注意:有時候我們可以直接忽略第二個參數,vue會根據傳遞參數的數據類型,自動判斷傳遞的是第幾個參數,如果傳遞對象就是第二個參數,如果傳遞數組或字符串就是第三個參數。
```
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一個 HTML 標簽名、組件選項對象,或者
// resolve 了上述任何一種的一個 async 函數。必填項。
'div',
// {Object}
// 一個與模板中屬性對應的數據對象。可選。
{
// (詳情見下一節)
},
// {String | Array}
// 子虛擬節點 (VNodes),由 `createElement()` 構建而成,
// 也可以使用字符串來生成“文本虛擬節點”。可選。
[
'先寫一些文字',
createElement('h1', '一則頭條'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
```
## **二:createElement()的第二個參數——數據對象**
在使用模版中,我們綁定屬性使用到`v-bind:class`和`v-bind:style`這樣的語法,但在虛擬DOM元素中,這樣的語法將會寫在數據對象中。該對象可以綁定普通的 HTML 特性,也可以綁定如`innerHTML`這樣的 DOM 屬性 (這會覆蓋`v-html`指令)
### **數據對象:**
```
{
// class綁定 等價于 v-bind:class
// 接受一個字符串、對象或字符串和對象組成的數組
'class': {
foo: true,
bar: false
},
// 樣式綁定,等價于 v-bind:style
// 接受一個字符串、對象,或對象組成的數組
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML 特性
attrs: {
id: 'foo'
},
// 組件 prop
props: {
myProp: 'bar'
},
// DOM 屬性
domProps: {
innerHTML: 'baz'
},
// 事件監聽寫在`on`屬性中
// 但不再支持如 `v-on:keyup.enter` 這樣的修飾器。
// 需要在處理函數中手動檢查 keyCode。
on: {
click: this.clickHandler
},
// 僅用于組件,用于監聽原生事件,而不是組件內部使用
// `vm.$emit` 觸發的事件。
// 監聽自定義事件
nativeOn: {
click: this.nativeClickHandler
},
// 自定義指令。注意,你無法對 `binding` 中的 `oldValue`
// 賦值,因為 Vue 已經自動為你進行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式為
// { name: props => VNode | Array<VNode> }
// 函數參數是傳遞的數據
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果組件是其它組件的子組件,需為插槽指定名稱
slot: 'name-of-slot',
// 其它特殊頂層屬性
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函數中給多個元素都應用了相同的 ref 名,
// 那么 `$refs.myRef` 會變成一個數組。
refInFor: true
}
```
常用的數據對象選項:
1. class:控制類名
2. style :樣式
3. attrs :用來寫正常的 html 屬性 id src 等等
4. domProps :用來寫原生的dom 屬性
5. on:用來寫原生方法
>[info] 在使用時,有時候我們會省略第二個參數,只寫第一、第三個參數。那么為什么能跳過第二個參數呢?關鍵就在于參數的數據類型。第二個參數是**對象**類型,第三個參數是**字符串**或者**數組**,vue內部根據參數的類型判斷傳入的是那個參數。
實例:
**HTML模版**:
```
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
```
**使用渲染函數**:
```
Vue.component('anchored-heading', {
render: function (createElement) {
// 創建 kebab-case 風格的 ID
var headingId = getChildrenTextContent(this.$slots.default)
.toLowerCase()
.replace(/\W+/g, '-')
.replace(/(^-|-$)/g, '')
return createElement(
'h' + this.level, //h標簽
[
createElement('a', { //h標簽內部的a標簽
attrs: {
name: headingId, //a標簽的name屬性
href: '#' + headingId //a標簽的href屬性
}
}, this.$slots.default) //a標簽里面的內容,這里傳入的是Hello world!
]
)
},
props: {
level: {
type: Number,
required: true
}
}
```
### **約束**
**虛擬節點必須唯一**
組件中的所有 VNode 必須是唯一的。例如,下面的渲染函數是不合法的:
```
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 錯誤 - 重復的 VNode
myParagraphVNode, myParagraphVNode
])
}
```
如果需要渲染重復的虛擬節點,可以使用工廠函數實現:
```
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
```
## **三:使用 JavaScript 代替模板功能**
渲染函數本身就是一個函數,所以可以寫任意JavaScript代碼。
### **1.`v-if`和 `v-for`**
在模板中使用的 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>
```
這些都可以在渲染函數中用 JavaScript 的 if/else 和 map 來重寫:
```
props: ['items'],
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.')
}
}
```
### **2.`v-model`**
渲染函數中沒有與`v-model`的直接對應——你必須自己實現相應的邏輯:
```
props: ['value'],
render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.$emit('input', event.target.value)
}
}
})
}
```
等價于模版:
```
<input value="value" @input="$emit('input',$event.target.value)"></input>
```
這就是深入底層的代價,但與 v-model 相比,這可以讓你更好地控制交互細節。
### **3.事件 & 按鍵修飾符**
對于`.passive`、`.capture`和`.once`這些事件修飾符, Vue 提供了相應的前綴可以用于`on`:
| 修飾符 | 前綴 |
| --- | --- |
| `.passive` | `&` |
| `.capture` | `!` |
| `.once` | `~` |
| `.capture.once`或 | |
| `.once.capture` | `~!` |
例如:
~~~
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
~~~
對于所有其它的修飾符,私有前綴都不是必須的,因為你可以在事件處理函數中使用事件方法:
| 修飾符 | 處理函數中的等價操作 |
| --- | --- |
| `.stop` | `event.stopPropagation()` |
| `.prevent` | `event.preventDefault()` |
| `.self` | `if (event.target !== event.currentTarget) return` |
| 按鍵: | |
| `.enter`,`.13` | `if (event.keyCode !== 13) return`(對于別的按鍵修飾符來說,可將`13`改為[另一個按鍵碼](http://keycode.info/)) |
| 修飾鍵: | |
| `.ctrl`,`.alt`,`.shift`,`.meta` | `if (!event.ctrlKey) return`(將`ctrlKey`分別修改為`altKey`、`shiftKey`或者`metaKey`) |
這里是一個使用所有修飾符的例子:
~~~
on: {
keyup: function (event) {
? ?// 如果觸發事件的元素不是事件綁定的元素
? ?// 則返回
? ?if (event.target !== event.currentTarget) return
? ?// 如果按下去的不是 enter 鍵或者
? ?// 沒有同時按下 shift 鍵
? ?// 則返回
? ?if (!event.shiftKey || event.keyCode !== 13) return
? ?// 阻止?事件冒泡
? event.stopPropagation()
? ?// 阻止該元素默認的 keyup 事件
? ?event.preventDefault()
// ...
}
}
~~~
### **4.插槽**
可以通過 `this.$slots` 訪問靜態插槽的內容,每個插槽都是一個`VNode` 數組
模版寫法:
```
<div><slot></slot></div>
```
渲染函數寫法:
```
render: function (createElement) {
return createElement('div', this.$slots.default) //this.$slots.default是默認插槽的內容,注意是虛擬節點數組
}
```
也可以通過`this.$scopedSlots`訪問作用域插槽,每個作用域插槽都是一個返回若干 VNode 的函數:
模版寫法:
```
<div><slot :text="message"></slot></div>
```
渲染函數寫法:
~~~
props: ['message'],
render: function (createElement) {
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
~~~
如果要用渲染函數向子組件中傳遞作用域插槽,可以利用 VNode 數據對象中的`scopedSlots`字段:
模版寫法:
```
<div>
<child slot-scope="default">
<span>{{default.text}}</span>
</child>
</div>
```
渲染函數寫法:
~~~
render: function (createElement) {
return createElement('div', [
createElement('child', {
// 在數據對象中傳遞 `scopedSlots`
// 格式為 { name: props => VNode | Array<VNode> }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
~~~
## **四:JSX**
如果你寫了很多`render`函數,可能會覺得下面這樣的代碼寫起來很痛苦:
~~~
createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
~~~
特別是對應的模板如此簡單的情況下:
~~~
<anchored-heading :level="1">
<span>Hello</span> world!
</anchored-heading>
~~~
這就是為什么會有一個[Babel 插件](https://github.com/vuejs/jsx),用于在 Vue 中使用 JSX 語法,它可以讓我們回到更接近于模板的語法上。
~~~
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
~~~
>[danger] 將`h`作為`createElement`的別名是 Vue 生態系統中的一個通用慣例,實際上也是 JSX 所要求的。從 Vue 的 Babel 插件的[3.4.0 版本](https://github.com/vuejs/babel-plugin-transform-vue-jsx#h-auto-injection)開始,我們會在以 ES5 語法聲明的含有 JSX 的任何方法和 getter 中 (不是函數或箭頭函數中) 自動注入`const h = this.$createElement`,這樣你就可以去掉`(h)`參數了。對于更早版本的插件,如果`h`在當前作用域中不可用,應用會拋錯。