渲染函數 & JSX
目錄
- ##節點、樹狀圖以及虛擬DOM
- ##完整範例
- 約束
slots() 和children 對比
##範本編譯
#基礎
<h1> <a name="hello-world" href="#hello-world"> Hello world! </a> </h1>###對於上面的HTML,你決定這樣定義元件介面:###
<anchored-heading :level="1">Hello world!</anchored-heading>###當開始寫一個只能透過###level## # prop 動態產生標題(heading) 的元件時,你可能很快想到這樣實作:###
<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>
Vue.component('anchored-heading', { template: '#anchored-heading-template', props: { level: { type: Number, required: true } } })###這裡用模板並不是最好的選擇:不但程式碼冗長,而且在每個層級的標題中重複書寫了###<slot></slot>###,在要插入錨點元素時還要再重複一次。 ######雖然模板在大多數元件中都非常好用,但是顯然在這裡它就不合適了。那麼,我們來嘗試使用 ###render### 函數來重寫上面的範例:###
Vue.component('anchored-heading', { render: function (createElement) { return createElement( 'h' + this.level, // 标签名称 this.$slots.default // 子节点数组 ) }, props: { level: { type: Number, required: true } } })
看起來簡單多了!這樣程式碼精簡很多,但是需要非常熟悉 Vue 的實例屬性。在這個例子中,你需要知道,傳遞不帶v-slot
指令的子節點在元件中時,例如anchored-heading
中的Hello world!
,這些子節點被儲存在元件實例中的$slots.default
中。如果你還不了解,在深入渲染函數之前推薦閱讀實例屬性 API。
節點、樹以及虛擬DOM
#在深入渲染函數之前,先了解一些瀏覽器的工作原理是很重要的。以下面這段HTML 為例:
<div> <h1>My title</h1> Some text content <!-- TODO: Add tagline --> </div>
當瀏覽器讀到這些程式碼時,它會建立一個「DOM 節點」樹來保持追蹤所有內容,如同你會畫一張家譜樹來追蹤家庭成員的發展一樣。
上述 HTML 對應的 DOM 節點樹如下圖所示:
#每個元素都是節點。每段文字也是一個節點。甚至註解也都是節點。一個節點就是頁面的一個部分。就像家譜樹一樣,每個節點都可以有孩子節點 (也就是說每個部分可以包含其它的一些部分)。
有效率地更新所有這些節點會是比較困難的,不過所幸你不必手動完成這個工作。你只需要告訴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 node)”,也常簡寫它為“VNode”。 「虛擬 DOM」是我們對由 Vue 元件樹建立起來的整個 VNode 樹的稱呼。
createElement
參數
接下來你需要熟悉的是如何在
函數中使用範本中的那些功能。這裡是createElement 接受的參數:
// @returns {VNode} createElement( // {String | Object | Function} // 一个 HTML 标签名、组件选项对象,或者 // resolve 了上述任何一种的一个 async 函数。必填项。 'div', // {Object} // 一个与模板中属性对应的数据对象。可选。 { // (详情见下一节) }, // {String | Array} // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成, // 也可以使用字符串来生成“文本虚拟节点”。可选。 [ '先写一些文字', createElement('h1', '一则头条'), createElement(MyComponent, { props: { someProp: 'foobar' } }) ] )
#深入資料物件
##有一點要注意:正如
v-bind:class
v-bind:style
在模板語法中會被特別對待一樣,它們在VNode 資料物件中也有對應的頂層字段。這個物件也允許你綁定普通的 HTML 特性,也允許綁定如
這樣的 DOM 屬性 (這會覆寫 v-html 指令)。 {
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
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
}
完整範例
有了這些知識,我們現在可以完成我們最開始想實現的元件: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
}
}
})
約束
VNode 必須唯一
元件樹中的所有VNode 必須是唯一的。這意味著,下面的渲染函數是不合法的:render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 错误 - 重复的 VNode
myParagraphVNode, myParagraphVNode
])
}
如果你真的需要重複很多次的元素/元件,你可以使用工廠函數來實現。例如,下面這渲染函數以完全合法的方式渲染了20 個相同的段落:render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
v-if
## 和v-for
只要在原生的JavaScript 中可以輕鬆完成的操作,Vue 的渲染函數就不會提供專有的替代方法。例如,在模板中使用的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.')
}
}
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) } } }) }這就是深入底層的代價,但與
v-model 相比,這可以讓你更好地控制互動細節。
事件& 按鍵修飾符
#對於
.passive
、
修飾符 | 前綴 |
---|---|
#.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 改為另一個按鍵碼) |
#修飾鍵:.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() // ... } }
#插槽
你可以透過this.$slots
存取靜態插槽的內容,每個插槽都是VNode 陣列:
render: function (createElement) { // `<div><slot></slot></div>` return createElement('div', this.$slots.default) }
也可以透過this.$scopedSlots
存取作用域插槽,每個作用域插槽都是一個傳回若干VNode 的函數:
props: ['message'], render: function (createElement) { // `<div><slot :text="message"></slot></div>` return createElement('div', [ this.$scopedSlots.default({ text: this.message }) ]) }
如果要用渲染函數傳送作用域插槽,可以利用VNode 資料物件中的scopedSlots
欄位:
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 外掛程式,用於在Vue 中使用JSX 語法,它可以讓我們回到更接近模板的語法。
import AnchoredHeading from './AnchoredHeading.vue' new Vue({ el: '#demo', render: function (h) { return ( <AnchoredHeading level={1}> <span>Hello</span> world! </AnchoredHeading> ) } })
將
h 作為 createElement 的別名是 Vue 生態系統中的一個一般慣例,實際上也是 JSX 所要求的。從 Vue 的 Babel 外掛程式的 3.4.0 版本
開始,我們會在以 ES2015 語法宣告的含有 JSX 的任何方法和 getter 中 (不是函數或箭頭函數中) this.$createElement,這樣你就可以去掉
(h)
參數了。對於更早版本的插件,如果
在目前作用域中不可用,應用會拋錯。 要了解更多關於 JSX 如何對應到 JavaScript,請閱讀使用文件
。
###函數式元件################先前建立的錨點標題元件是比較簡單,沒有管理任何狀態,也沒有監聽任何傳遞給它的狀態,也沒有生命週期方法。實際上,它只是一個接受一些 prop 的函數。 #########在這樣的場景下,我們可以將元件標記為###functional###,這意味著它無狀態(沒有###響應式資料###),也沒有實例(沒有###this### 上下文)。 ######一個###函數式元件###就像這樣:###
Vue.component('my-component', { functional: true, // Props 是可选的 props: { // ... }, // 为了弥补缺少的实例 // 提供第二个参数作为上下文 render: function (createElement, context) { // ... } })
注意:在 2.3.0 之前的版本中,如果一個函數式元件想要接收 prop,則
props
選項是必須的。在 2.3.0 或以上的版本中,你可以省略props
選項,所有元件上的特性都會被自動隱式解析為 prop。
當使用函數式元件時,該引用將會是 HTMLElement,因為他們是無狀態的也是無實例的。
在2.5.0 以上版本中,如果你使用了單一檔案元件,那麼基於範本的函數式元件可以這樣宣告:
<template functional> </template>
元件所需的一切都是透過context
參數傳遞,它是一個包含以下欄位的物件:
##props
:提供所有prop 的物件
children
: VNode 子節點的陣列
slots
:一個函數,傳回了包含所有插槽的物件
scopedSlots
:(2.6.0 ) 一個暴露傳入的作用域插槽的物件。也以函數形式暴露普通插槽。
data
:傳遞給元件的整個
資料物件,作為createElement的第二個參數傳入元件
parent
:對父元件的參考
#listeners
:(2.3.0 )一個包含了所有父元件為目前元件註冊的事件監聽器的物件。這是 data.on 的一個別名。
injections
:(2.3.0 ) 如果使用了
inject
選項,則該物件包含了應當被注入的屬性。
functional: true 之後,需要更新我們的錨點標題元件的渲染函數,為其增加
context 參數,並將
this.$slots.default 更新為
context.children,然後將
this.level 更新為
context.props.level。
- 程式化地在多個元件中選擇一個來代為渲染;
- 在將
children
、
props、
data傳遞給子元件之前操作它們。
smart-list 元件的例子,它能根據傳入prop 的值來代為渲染更具體的元件:
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 的特性會自動加入到元件的根元素上,將現有的同名特性進行替換或與其進行智慧合併。
然而函數式元件要求你明確定義該行為:
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
的別名) 傳遞任何事件監聽器。
<template functional> <button class="btn btn-primary" v-bind="data.attrs" v-on="listeners" > <slot/> </button> </template>
slots()
## 和children 對比
slots() 和
children。
slots().default 不是跟
children 類似的嗎?在一些場景中,是這樣——但如果是如下的帶有子節點的函數式元件呢?
<my-functional-component> <p v-slot:foo> first </p> <p>second</p> </my-functional-component>對於這個元件,
children 會給你兩個段落標籤,而
slots().default 只會傳遞第二個匿名段落標籤,
slots().foo 會傳遞第一個具名段落標籤。同時擁有
children 和
slots(),因此你可以選擇讓元件感知某個插槽機制,還是簡單地透過傳遞
children,移交給其它元件去處理。
模板編譯
#你可能會有興趣知道,Vue 的模板實際上被編譯成了渲染函數。這是一個實作細節,通常不需要關心。但如果你想看看模板的功能具體是怎麼被編譯的,可能會發現會很有趣。以下是使用
Vue.compile 來即時編譯模板字串的簡單範例: