## 基礎
Vue 推薦在絕大多數情況下使用 template 來創建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力,這時你可以用 **render 函數**,它比 template 更接近編譯器。
讓我們深入一個簡單的例子,這個例子里 `render` 函數很實用。假設我們要生成錨點標題 (anchored headings):
``` html
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
```
對于上面的 HTML,我們決定這樣定義組件接口:
``` html
<anchored-heading :level="1">Hello world!</anchored-heading>
```
當我們開始寫一個只能通過 `level` prop 動態生成 heading 標簽的組件時,你可能很快想到這樣實現:
``` html
<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
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
```
在這種場景中使用 template 并不是最好的選擇:首先代碼冗長,為了在不同級別的標題中插入錨點元素,我們需要重復地使用 `<slot></slot>`。
雖然模板在大多數組件中都非常好用,但是在這里它就不是很簡潔的了。那么,我們來嘗試使用 `render` 函數重寫上面的例子:
``` js
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 標簽名稱
this.$slots.default // 子元素數組
)
},
props: {
level: {
type: Number,
required: true
}
}
})
```
簡單清晰很多!簡單來說,這樣代碼精簡很多,但是需要非常熟悉 Vue 的實例屬性。在這個例子中,你需要知道,向組件中傳遞不帶 `slot` 特性的子元素時,比如 `anchored-heading` 中的 `Hello world!`,這些子元素被存儲在組件實例中的 `$slots.default` 中。如果你還不了解,**在深入 render 函數之前推薦閱讀[實例屬性 API](../api/#實例屬性)。**
## 節點、樹以及虛擬 DOM
在深入渲染函數之前,了解一些瀏覽器的工作原理是很重要的。以下面這段 HTML 為例:
```html
<div>
<h1>My title</h1>
Some text content
<!-- TODO: 添加標簽行 -->
</div>
```
當瀏覽器讀到這些代碼時,它會建立一個[“DOM 節點”樹](https://javascript.info/dom-nodes)來保持追蹤,如同你會畫一張家譜樹來追蹤家庭成員的發展一樣。
HTML 的 DOM 節點樹如下圖所示:

每個元素都是一個節點。每片文字也是一個節點。甚至注釋也都是節點。一個節點就是頁面的一個部分。就像家譜樹一樣,每個節點都可以有孩子節點 (也就是說每個部分可以包含其它的一些部分)。
高效的更新所有這些節點會是比較困難的,不過所幸你不必再手動完成這個工作了。你只需要告訴 Vue 你希望頁面上的 HTML 是什么,這可以是在一個模板里:
```html
<h1>{{ blogTitle }}</h1>
```
或者一個渲染函數里:
``` js
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
```
在這兩種情況下,Vue 都會自動保持頁面的更新,即便 `blogTitle` 發生了改變。
### 虛擬 DOM
Vue 通過建立一個**虛擬 DOM** 對真實 DOM 發生的變化保持追蹤。請仔細看這行代碼:
``` js
return createElement('h1', this.blogTitle)
```
`createElement` 到底會返回什么呢?其實不是一個*實際的* DOM 元素。它更準確的名字可能是 `createNodeDescription`,因為它所包含的信息會告訴 Vue 頁面上需要渲染什么樣的節點,及其子節點。我們把這樣的節點描述為“虛擬節點 (Virtual Node)”,也常簡寫它為“VNode”。“虛擬 DOM”是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼。
## `createElement` 參數
接下來你需要熟悉的是如何在 `createElement` 函數中生成模板。這里是 `createElement` 接受的參數:
``` js
// @returns {VNode}
createElement(
// {String | Object | Function}
?// 一個 HTML 標簽字符串,組件選項對象,或者
// 解析上述任何一種的一個 async 異步函數。必需參數。
?'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` 指令)。
``` js
{
// 和`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
},
?// 自定義指令。注意,你無法對 `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
}
```
### 完整示例
有了這些知識,我們現在可以完成我們最開始想實現的組件:
``` js
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) {
? ?// 創建 kebab-case 風格的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 必須唯一
組件樹中的所有 VNodes 必須是唯一的。這意味著,下面的 render function 是無效的:
``` js
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
? ?// 錯誤-重復的 VNodes
? ?myParagraphVNode, myParagraphVNode
])
}
```
如果你真的需要重復很多次的元素/組件,你可以使用工廠函數來實現。例如,下面這個例子 render 函數完美有效地渲染了 20 個相同的段落:
``` js
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
```
## 使用 JavaScript 代替模板功能
### `v-if` 和 `v-for`
只要在原生的 JavaScript 中可以輕松完成的操作,Vue 的 render 函數就不會提供專有的替代方法。比如,在 template 中使用的 `v-if` 和 `v-for`:
``` html
<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` 重寫:
``` js
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.')
}
}
```
### `v-model`
render 函數中沒有與 `v-model` 的直接對應 - 你必須自己實現相應的邏輯:
``` js
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)
}
}
})
}
```
這就是深入底層的代價,但與 `v-model` 相比,這可以讓你更好地控制交互細節。
### 事件 & 按鍵修飾符
對于 `.passive`、`.capture` 和 `.once`事件修飾符, Vue 提供了相應的前綴可以用于 `on`:
| Modifier(s) | Prefix |
| ------ | ------ |
| `.passive` | `&` |
| `.capture` | `!` |
| `.once` | `~` |
| `.capture.once` or<br>`.once.capture` | `~!` |
例如:
```javascript
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:<br>`.enter`, `.13` | `if (event.keyCode !== 13) return` (change `13` to [another key code](http://keycode.info/) for other key modifiers) |
| Modifiers Keys:<br>`.ctrl`, `.alt`, `.shift`, `.meta` | `if (!event.ctrlKey) return` (change `ctrlKey` to `altKey`, `shiftKey`, or `metaKey`, respectively) |
這里是一個使用所有修飾符的例子:
```javascript
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`](../api/#vm-slots) 訪問靜態插槽的內容,得到的是一個 VNodes 數組:
``` js
render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
```
也可以通過 [`this.$scopedSlots`](../api/#vm-scopedSlots) 訪問作用域插槽,得到的是一個返回 VNodes 的函數:
``` js
props: ['message'],
render: function (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
```
如果要用渲染函數向子組件中傳遞作用域插槽,可以利用 VNode 數據對象中的 `scopedSlots` 域:
``` js
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` 函數,可能會覺得下面這樣的代碼寫起來很痛苦:
``` js
createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
```
特別是模板如此簡單的情況下:
``` html
<anchored-heading :level="1">
<span>Hello</span> world!
</anchored-heading>
```
這就是為什么會有一個 [Babel 插件](https://github.com/vuejs/babel-plugin-transform-vue-jsx),用于在 Vue 中使用 JSX 語法,它可以讓我們回到更接近于模板的語法上。
``` js
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
```
<p class="tip">將 `h` 作為 `createElement` 的別名是 Vue 生態系統中的一個通用慣例,實際上也是 JSX 所要求的,如果在作用域中 `h` 失去作用,在應用中會觸發報錯。</p>
更多關于 JSX 映射到 JavaScript,閱讀 [使用文檔](https://github.com/vuejs/babel-plugin-transform-vue-jsx#usage)。
## 函數式組件
之前創建的錨點標題組件是比較簡單,沒有管理或者監聽任何傳遞給他的狀態,也沒有生命周期方法。它只是一個接收參數的函數。
在這個例子中,我們標記組件為 `functional`,這意味它是無狀態 (沒有[響應式數據](../api/#選項-數據)),無實例 (沒有 `this` 上下文)。
一個**函數式組件**就像這樣:
``` js
Vue.component('my-component', {
functional: true,
// Props 可選
props: {
// ...
},
// 為了彌補缺少的實例
// 提供第二個參數作為上下文
render: function (createElement, context) {
// ...
}
})
```
> 注意:在 2.3.0 之前的版本中,如果一個函數式組件想要接受 props,則 `props` 選項是必須的。在 2.3.0 或以上的版本中,你可以省略 `props` 選項,所有組件上的特性都會被自動解析為 props。
在 2.5.0 及以上版本中,如果你使用了[單文件組件](single-file-components.html),那么基于模板的函數式組件可以這樣聲明:
``` html
<template functional>
</template>
```
組件需要的一切都是通過上下文傳遞,包括:
- `props`:提供所有 prop 的對象
- `children`: VNode 子節點的數組
- `slots`: 返回所有插槽的對象的函數
- `data`:傳遞給組件的[數據對象](#深入-data-對象),作為 `createElement` 的第二個參數傳入組件
- `parent`:對父組件的引用
- `listeners`: (2.3.0+) 一個包含了所有在父組件上注冊的事件偵聽器的對象。這只是一個指向 `data.on` 的別名。
- `injections`: (2.3.0+) 如果使用了 [`inject`](../api/#provide-inject) 選項,則該對象包含了應當被注入的屬性。
在添加 `functional: true` 之后,錨點標題組件的 render 函數之間簡單更新增加 `context` 參數,`this.$slots.default` 更新為 `context.children`,之后`this.level` 更新為 `context.props.level`。
因為函數式組件只是一個函數,所以渲染開銷也低很多。然而,對持久化實例的缺乏也意味著函數式組件不會出現在 [Vue devtools](https://github.com/vuejs/vue-devtools) 的組件樹里。
在作為包裝組件時它們也同樣非常有用,比如,當你需要做這些時:
- 程序化地在多個組件中選擇一個
- 在將 children, props, data 傳遞給子組件之前操作它們。
下面是一個依賴傳入 props 的值的 `smart-list` 組件例子,它能代表更多具體的組件:
``` js
var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
},
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
)
}
})
```
### 向子元素或子組件傳遞特性和事件
在普通組件中,沒有被定義為 prop 的特性會自動添加到組件的根元素上,將現有的同名特性替換或與其[智能合并](class-and-style.html)。
然而函數式組件要求你顯式定義該行為:
```js
Vue.component('my-functional-button', {
functional: true,
render: function (createElement, context) {
// 完全透明的傳入任何特性、事件監聽器、子結點等。
return createElement('button', context.data, context.children)
}
})
```
向 `createElement` 通過傳入 `context.data` 作為第二個參數,我們就把 `my-functional-button` 上面所有的特性和事件監聽器都傳遞下去了。事實上這是非常透明的,那些事件甚至并不要求 `.native` 修飾符。
如果你使用基于模板的函數式組件,那么你還需要手動添加特性和監聽器。因為我們可以訪問到其獨立的上下文內容,所以我們可以使用 `data.attrs` 傳遞任何 HTML 特性,也可以使用 `listeners` *(即 `data.on` 的別名)* 傳遞任何事件監聽器。
```html
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners"
>
<slot/>
</button>
</template>
```
### `slots()` 和 `children` 對比
你可能想知道為什么同時需要 `slots()` 和 `children`。`slots().default` 不是和 `children` 類似的嗎?在一些場景中,是這樣,但是如果是函數式組件和下面這樣的 children 呢?
``` html
<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:
- 寫在前面
- 基礎
- 安裝
- 介紹
- Vue實例
- 模板語法
- 計算屬性和偵聽器
- Class 與 Style 綁定
- 條件渲染
- 列表渲染
- 事件處理
- 表單輸入綁定
- 組件基礎
- 深入了解組件
- 組件注冊
- Prop
- 自定義事件
- 插槽
- 動態組件 & 異步組件
- 處理邊界情況
- 過渡 & 動畫
- 進入/離開 & 列表過渡
- 狀態過渡
- 可復用性 & 組合
- 混入
- 自定義指令
- 渲染函數 & JSX
- 插件
- 過濾器
- 工具
- 生產環境部署
- 單文件組件
- 單元測試
- TypeScript 支持
- 規模化
- 路由
- 狀態管理
- 服務端渲染
- 內在
- 深入響應式原理
- 遷移
- 從 Vue 1.x 遷移
- 從 Vue Router 0.7.x 遷移
- 從 Vuex 0.6.x 遷移到 1.0
- 更多
- 對比其他框架
- 加入 Vue.js 社區
- 開發團隊