Vue.js 2.x render
渲染函數 & JSX
Vue絕大多數情況下使用template
創建 HTML。但是比如一些重復性比較高的場景,需要運用 JavaScript 的完全編程能力,可以使用render
函數。
1. 節點、樹以及虛擬DOM
每個元素都是一個節點。每片文字也是一個節點。甚至注釋也都是節點。
一個節點就是頁面的一個部分。每個節點都可以有子節點。
比如上面的節點樹就表示下面的代碼:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: 添加標簽行 -->
</div>
1.1 虛擬 DOM
Vue 通過建立一個虛擬 DOM 對真實 DOM 發生的變化保持追蹤。
虛擬 DOM 是對Vue組件樹建立起來的整個 VNode(Virtual Node)樹的稱呼。
1.1.1 createElement 參數
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一個 HTML 標簽字符串,組件選項對象,或者
// 解析上述任何一種的一個 async 異步函數
// 必須參數
'div',
// {Object}
// 一個包含模板相關屬性的數據對象
// 你可以在 template 中使用這些特性
// 可選參數
{
// 屬性參見 1.1.2 深入 data 對象
},
// {String | Array}
// 子虛擬節點(VNodes) 由 createElement() 構建而成
// 也可以使用字符串來生成“文本虛擬節點”
// 可選參數
[
'文字',
createElement('h1', '一個標題'),
createElement(MyComponent, {
props: {
someProp: 'foo'
}
})
]
)
1.1.2 深入 data 對象
在 VNode 數據對象中,下列屬性名是級別最高的字段。該對象也允許綁定普通的 HTML 特性,就像 DOM 屬性一樣,比如innerHTML
(這會取代v-html
指令)。
{
// 和 v-bind:calss 一樣的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
}
1.1.3 完整示例
一個生成錨點標題的組件:
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
}
}
})
使用效果:
<div id="app">
<anchored-heading :level="1">first</anchored-heading>
<anchored-heading :level="2">second</anchored-heading>
<anchored-heading :level="3">
third
<small>thirdSub</small>
</anchored-heading>
</div>
1.1.4 約束
組件樹中的所有 VNodes 必須是唯一的。
比如這樣寫是錯誤的:
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 錯誤-重復的 VNodes
myParagraphVNode, myParagraphVNode
])
}
如果需要重復多次元素/組件,可以用工廠函數來實現:
render: function(createElement) {
return createElement('div',
Array.apply(null, { length: 20 }.map(function() {
return createElement('p', 'hello')
}))
);
}
1.2 使用 JavaScript 代替模板功能
1.2.1 v-if 和 v-for
只要在原生的 JavaScript 中可以輕松完成的操作,Vue的render函數就不會提供專有的替代方法。
比如這里的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 方法重寫:
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 here.');
}
}
1.2.2 v-model
render 函數中沒有與 v-model
直接對應的方法,需要手動實現:
props: ['value'],
render: function (createElement) {
var self = this;
return createElement('input', {
// DOM屬性
domProps: {
value: self.value
},
on: {
input: function(e) {
self.$emit('input', e.target.value);
}
}
})
}
1.3 事件&按鍵修飾符
對於.passive
、.capture
、.once
事件修飾符,Vue提供了相應的前綴可以用於on
:
Modifier(s) | Prefix |
---|---|
.passive |
& |
.capture |
! |
.once |
~ |
.capture.once 或.once.capture |
~! |
比如:
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
對於其他的修飾符,可以在事件處理函數中使用事件方法:
Modifier(s) | Equivalent in Handler |
---|---|
.stop |
e.stopPropagation() |
.prevent |
e.preventDefault() |
.self |
if (e.target !== e.currentTarget) return; |
.enter , .13 |
e.keyCode === 13 |
比如:
on: {
keyup: function (event) {
// 如果觸發事件的元素不是事件綁定的元素
// 則返回
if (event.target !== event.currentTarget) return
// 如果按下去的不是 enter 鍵或者
// 沒有同時按下 shift 鍵
// 則返回
if (!event.shiftKey || event.keyCode !== 13) return
// 阻止 事件冒泡
event.stopPropagation()
// 阻止該元素默認的 keyup 事件
event.preventDefault()
// ...
}
}
1.4 插槽
可以通過this.$slots
訪問靜態插槽內容,得到的是一個 VNodes 數組:
render: function(createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
也可以通過this.$scopeSlots
訪問作用域插槽,得到的是一個 VNodes 的函數:
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)
}
}
})
])
}
2. JSX
在Vue中使用JSX語法,可以避免書寫繁雜冗余的render代碼,Babel插件用於在Vue中使用JSX語法。
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#app',
render: function(h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world
</AnchoredHeading>
)
}
})
3. 函數式組件
一個函數是組件,意味着它是無狀態(沒有響應式數據),無實例(沒有this上下文)的:
Vue.component('my-component', {
functional: true,
// props可選 v2.3.0^
props: {},
// 為了彌補缺少的實例
// 提供第二個參數作為上下文
render(createElement, context) {
}
})
在v2.5.0^版本中,單文件組件基於模板的函數式組件可以這樣聲明:
<template functional></template>
組件需要的一切都是通過上下文來傳遞:
props
提供所有prop對象children
VNode子節點的數組slots
返回所有插槽的對象的函數scopedSlots
一個暴露傳入的作用域插槽以及函數形式的普通插槽的對象data
傳遞給組件的數據對象,作為CreateElement的第二個參數傳入組件parent
對父組件的引用listeners
一個包含了所有在父組件上注冊的事件偵聽器的對象,data.on
的別名injections
如果使用了inject選項,該對象包含了應當被注入的屬性
之前的錨點標題組件可以改為如下的函數式組件:
...
Vue.component('anchored-heading', {
functional: true,
render: function (createElement, context) {
// 創建 kebab-case 風格的ID
var headingId = getChildrenTextContent(context.children)
.toLowerCase()
.replace(/\W+/g, '-')
.replace(/(^-|-$)/g, '')
return createElement(
'h' + context.props.level,
[
createElement('a', {
attrs: {
name: headingId,
href: '#' + headingId
}
}, context.children)
]
)
},
props: {
level: {
type: Number,
required: true
}
}
})
3.1 想子組件或子元素傳遞特性和事件
在普通組件中,沒有被定義為 prop 的特性會自動添加到組件的根元素上,將現有的同名特性替換或智能合並。
而函數式組件要求必須顯示定義該行為:
Vue.component('my-functional-button', {
functional: true,
render: function(createElement, context) {
return createElement('button', context.data, context.children)
}
})
如果使用模板的函數式組件,還需要手動添加特性和監聽器。
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners"
>
<slot/>
</button>
</template>
3.2 slots()和children對比
總的來說個人理解,slots()
可以拿到具名插槽,children
拿到所有。
4. 模板編譯
The end
2019-8-13 15:45:59