更靈活的組件:Render 函數(shù)與 Functional Render
Vue.js 2.x 與 Vue.js 1.x 最大的區(qū)別就在于 2.x 使用了 Virtual DOM(虛擬 DOM)來(lái)更新 DOM 節(jié)點(diǎn),提升渲染性能胆数。
一般來(lái)說(shuō),我們寫(xiě) Vue.js 組件踩晶,模板都是寫(xiě)在 <template>
內(nèi)的巫财,但它并不是最終呈現(xiàn)的內(nèi)容斜棚,template 只是一種對(duì)開(kāi)發(fā)者友好的語(yǔ)法拄氯,能夠一眼看到 DOM 節(jié)點(diǎn)火诸,容易維護(hù)锦针,在 Vue.js 編譯階段,會(huì)解析為 Virtual DOM惭蹂。
與 DOM 操作相比伞插,Virtual DOM 是基于 JavaScript 計(jì)算的割粮,所以開(kāi)銷(xiāo)會(huì)小很多盾碗。下圖演示了 Virtual DOM 運(yùn)行的過(guò)程:
正常的 DOM 節(jié)點(diǎn)在 HTML 中是這樣的:
<div id="main">
<p>文本內(nèi)容</p>
<p>文本內(nèi)容</p>
</div>
用 Virtual DOM 創(chuàng)建的 JavaScript 對(duì)象一般會(huì)是這樣的:
const vNode = {
tag: 'div',
attributes: {
id: 'main'
},
children: [
// p 節(jié)點(diǎn)
]
}
vNode 對(duì)象通過(guò)一些特定的選項(xiàng)描述了真實(shí)的 DOM 結(jié)構(gòu)。
在 Vue.js 中舀瓢,對(duì)于大部分場(chǎng)景廷雅,使用 template 足以應(yīng)付,但如果想完全發(fā)揮 JavaScript 的編程能力,或在一些特定場(chǎng)景下(后文介紹)航缀,需要使用 Vue.js 的 Render 函數(shù)商架。
Render 函數(shù)
正如上文介紹的 Virtual DOM 示例一樣,Vue.js 的 Render 函數(shù)也是類(lèi)似的語(yǔ)法芥玉,需要使用一些特定的選項(xiàng)蛇摸,將 template 的內(nèi)容改寫(xiě)成一個(gè) JavaScript 對(duì)象。
對(duì)于初級(jí)前端工程師灿巧,或想快速建站的需求赶袄,直接使用 Render 函數(shù)開(kāi)發(fā) Vue.js 組件是要比 template 困難的,原因在于 Render 函數(shù)返回的是一個(gè) JS 對(duì)象抠藕,沒(méi)有傳統(tǒng) DOM 的層級(jí)關(guān)系饿肺,配合上 if、else盾似、for 等語(yǔ)句敬辣,將節(jié)點(diǎn)拆分成不同 JS 對(duì)象再組裝,如果模板復(fù)雜零院,那一個(gè) Render 函數(shù)是難讀且難維護(hù)的溉跃。所以,絕大部分組件開(kāi)發(fā)和業(yè)務(wù)開(kāi)發(fā)门粪,我們直接使用 template 語(yǔ)法就可以了喊积,并不需要特意使用 Render 函數(shù),那樣只會(huì)增加負(fù)擔(dān)玄妈,同時(shí)也放棄了 Vue.js 最大的優(yōu)勢(shì)(React 無(wú) template 語(yǔ)法)乾吻。
很多學(xué)習(xí) Vue.js 的開(kāi)發(fā)者在遇到 Render 函數(shù)時(shí)都有點(diǎn)”躲避“,或直接放棄這部分拟蜻,這并沒(méi)有問(wèn)題绎签,因?yàn)椴挥?Render 函數(shù),照樣可以寫(xiě)出優(yōu)秀的 Vue.js 程序酝锅。不過(guò)诡必,Render 函數(shù)并沒(méi)有想象中的那么復(fù)雜,只是配置項(xiàng)特別多搔扁,一時(shí)難以記住爸舒,但歸根到底,Render 函數(shù)只有 3 個(gè)參數(shù)稿蹲。
來(lái)看一組 template 和 Render 寫(xiě)法的對(duì)照:
<template>
<div id="main" class="container" style="color: red">
<p v-if="show">內(nèi)容 1</p>
<p v-else>內(nèi)容 2</p>
</div>
</template>
<script>
export default {
data () {
return {
show: false
}
}
}
</script>
export default {
data () {
return {
show: false
}
},
render: (h) => {
let childNode;
if (this.show) {
childNode = h('p', '內(nèi)容 1');
} else {
childNode = h('p', '內(nèi)容 2');
}
return h('div', {
attrs: {
id: 'main'
},
class: {
container: true
},
style: {
color: 'red'
}
}, [childNode]);
}
}
這里的 h
扭勉,即 createElement
,是 Render 函數(shù)的核心苛聘⊥垦祝可以看到忠聚,template 中的 v-if / v-else 等指令,都被 JS 的 if / else 替代了唱捣,那 v-for 自然也會(huì)被 for 語(yǔ)句替代两蟀。
h 有 3 個(gè)參數(shù),分別是:
-
要渲染的元素或組件震缭,可以是一個(gè) html 標(biāo)簽赂毯、組件選項(xiàng)或一個(gè)函數(shù)(不常用),該參數(shù)為必填項(xiàng)拣宰。示例:
// 1. html 標(biāo)簽 h('div'); // 2. 組件選項(xiàng) import DatePicker from '../component/date-picker.vue'; h(DatePicker);
對(duì)應(yīng)屬性的數(shù)據(jù)對(duì)象欢瞪,比如組件的 props、元素的 class徐裸、綁定的事件遣鼓、slot、自定義指令等重贺,該參數(shù)是可選的骑祟,上文所說(shuō)的 Render 配置項(xiàng)多,指的就是這個(gè)參數(shù)气笙。該參數(shù)的完整配置和示例次企,可以到 Vue.js 的文檔查看,沒(méi)必要全部記住潜圃,用到時(shí)查閱就好:createElement 參數(shù)缸棵。
-
子節(jié)點(diǎn),可選谭期,String 或 Array堵第,它同樣是一個(gè) h。示例:
[ '內(nèi)容', h('p', '內(nèi)容'), h(Component, { props: { someProp: 'foo' } }) ]
約束
所有的組件樹(shù)中隧出,如果 vNode 是組件或含有組件的 slot踏志,那么 vNode 必須唯一。以下兩個(gè)示例都是錯(cuò)誤的:
// 局部聲明組件
const Child = {
render: (h) => {
return h('p', 'text');
}
}
export default {
render: (h) => {
// 創(chuàng)建一個(gè)子節(jié)點(diǎn)胀瞪,使用組件 Child
const ChildNode = h(Child);
return h('div', [
ChildNode,
ChildNode
]);
}
}
{
render: (h) => {
return h('div', [
this.$slots.default,
this.$slots.default
])
}
}
重復(fù)渲染多個(gè)組件或元素针余,可以通過(guò)一個(gè)循環(huán)和工廠(chǎng)函數(shù)來(lái)解決:
const Child = {
render: (h) => {
return h('p', 'text');
}
}
export default {
render: (h) => {
const children = Array.apply(null, {
length: 5
}).map(() => {
return h(Child);
});
return h('div', children);
}
}
對(duì)于含有組件的 slot,復(fù)用比較復(fù)雜凄诞,需要將 slot 的每個(gè)子節(jié)點(diǎn)都克隆一份圆雁,例如:
{
render: (h) => {
function cloneVNode (vnode) {
// 遞歸遍歷所有子節(jié)點(diǎn),并克隆
const clonedChildren = vnode.children && vnode.children.map(vnode => cloneVNode(vnode));
const cloned = h(vnode.tag, vnode.data, clonedChildren);
cloned.text = vnode.text;
cloned.isComment = vnode.isComment;
cloned.componentOptions = vnode.componentOptions;
cloned.elm = vnode.elm;
cloned.context = vnode.context;
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
return cloned;
}
const vNodes = this.$slots.default === undefined ? [] : this.$slots.default;
const clonedVNodes = this.$slots.default === undefined ? [] : vNodes.map(vnode => cloneVNode(vnode));
return h('div', [
vNodes,
clonedVNodes
])
}
}
在 Render 函數(shù)里創(chuàng)建了一個(gè) cloneVNode 的工廠(chǎng)函數(shù)帆谍,通過(guò)遞歸將 slot 所有子節(jié)點(diǎn)都克隆了一份伪朽,并對(duì) VNode 的關(guān)鍵屬性也進(jìn)行了復(fù)制。
深度克隆 slot 并非 Vue.js 內(nèi)置方法既忆,也沒(méi)有得到推薦驱负,屬于黑科技,在一些特殊的場(chǎng)景才會(huì)使用到患雇,正常業(yè)務(wù)幾乎是用不到的跃脊。比如 iView 組件庫(kù)的穿梭框組件 Transfer,就用到了這種方法:
它的使用方法是:
<Transfer
:data="data"
:target-keys="targetKeys"
:render-format="renderFormat">
<div :style="{float: 'right', margin: '5px'}">
<Button size="small" @click="reloadMockData">Refresh</Button>
</div>
</Transfer>
示例中的默認(rèn) slot 是一個(gè) Refresh 按鈕苛吱,使用者只寫(xiě)了一遍酪术,但在 Transfer 組件中,是通過(guò)克隆 VNode 的方法翠储,顯示了兩遍绘雁。如果不這樣做,就要聲明兩個(gè)具名 slot援所,但是左右兩個(gè)的邏輯可能是完全一樣的庐舟,使用者就要寫(xiě)兩個(gè)一模一樣的 slot,這是不友好的住拭。
Render 函數(shù)的基本用法還有很多挪略,比如 v-model 的用法、事件和修飾符滔岳、slot 等杠娱,讀者可以到 Vue.js 文檔閱讀。Vue.js 渲染函數(shù)
Render 函數(shù)使用場(chǎng)景
上文說(shuō)到谱煤,一般情況下是不推薦直接使用 Render 函數(shù)的摊求,使用 template 足以,在 Vue.js 中刘离,使用 Render 函數(shù)的場(chǎng)景室叉,主要有以下 4 點(diǎn):
-
使用兩個(gè)相同 slot。在 template 中硫惕,Vue.js 不允許使用兩個(gè)相同的 slot太惠,比如下面的示例是錯(cuò)誤的:
<template> <div> <slot></slot> <slot></slot> </div> </template>
解決方案就是上文中講到的約束,使用一個(gè)深度克隆 VNode 節(jié)點(diǎn)的方法疲憋。
-
在 SSR 環(huán)境(服務(wù)端渲染)凿渊,如果不是常規(guī)的 template 寫(xiě)法,比如通過(guò) Vue.extend 和 new Vue 構(gòu)造來(lái)生成的組件實(shí)例缚柳,是編譯不過(guò)的埃脏,在前面小節(jié)也有所介紹∏锩Γ回顧上一節(jié)的
$Alert
組件的 notification.js 文件彩掐,當(dāng)時(shí)是使用 Render 函數(shù)來(lái)渲染 Alert 組件,如果改成另一種寫(xiě)法灰追,在 SSR 中會(huì)報(bào)錯(cuò)堵幽,對(duì)比兩種寫(xiě)法:// 正確寫(xiě)法 import Alert from './alert.vue'; import Vue from 'vue'; Alert.newInstance = properties => { const props = properties || {}; const Instance = new Vue({ data: props, render (h) { return h(Alert, { props: props }); } }); const component = Instance.$mount(); document.body.appendChild(component.$el); const alert = Instance.$children[0]; return { add (noticeProps) { alert.add(noticeProps); }, remove (name) { alert.remove(name); } } }; export default Alert;
// 在 SSR 下報(bào)錯(cuò)的寫(xiě)法 import Alert from './alert.vue'; import Vue from 'vue'; Alert.newInstance = properties => { const props = properties || {}; const div = document.createElement('div'); div.innerHTML = `<Alert ${props}></Alert>`; document.body.appendChild(div); const Instance = new Vue({ el: div, data: props, components: { Alert } }); const alert = Instance.$children[0]; return { add (noticeProps) { alert.add(noticeProps); }, remove (name) { alert.remove(name); } } }; export default Alert;
在 runtime 版本的 Vue.js 中狗超,如果使用 Vue.extend 手動(dòng)構(gòu)造一個(gè)實(shí)例,使用 template 選項(xiàng)是會(huì)報(bào)錯(cuò)的朴下,在第 9 節(jié)中也有所介紹努咐。解決方案也很簡(jiǎn)單,把 template 改寫(xiě)為 Render 就可以了殴胧。需要注意的是渗稍,在開(kāi)發(fā)獨(dú)立組件時(shí),可以通過(guò)配置 Vue.js 版本來(lái)使 template 選項(xiàng)可用团滥,但這是在自己的環(huán)境竿屹,無(wú)法保證使用者的 Vue.js 版本,所以對(duì)于提供給他人用的組件灸姊,是需要考慮兼容 runtime 版本和 SSR 環(huán)境的拱燃。
這可能是使用 Render 函數(shù)最重要的一點(diǎn)。一個(gè) Vue.js 組件力惯,有一部分內(nèi)容需要從父級(jí)傳遞來(lái)顯示扼雏,如果是文本之類(lèi)的,直接通過(guò)
props
就可以夯膀,如果這個(gè)內(nèi)容帶有樣式或復(fù)雜一點(diǎn)的 html 結(jié)構(gòu)诗充,可以使用v-html
指令來(lái)渲染,父級(jí)傳遞的仍然是一個(gè) HTML Element 字符串诱建,不過(guò)它僅僅是能解析正常的 html 節(jié)點(diǎn)且有 XSS 風(fēng)險(xiǎn)蝴蜓。當(dāng)需要最大化程度自定義顯示內(nèi)容時(shí),就需要Render
函數(shù)俺猿,它可以渲染一個(gè)完整的 Vue.js 組件茎匠。你可能會(huì)說(shuō),用 slot 不就好了押袍?的確诵冒,slot 的作用就是做內(nèi)容分發(fā)的,但在一些特殊組件中谊惭,可能 slot 也不行汽馋。比如一個(gè)表格組件Table
,它只接收兩個(gè) props:列配置 columns 和行數(shù)據(jù) data圈盔,不過(guò)某一列的單元格豹芯,不是只將數(shù)據(jù)顯示出來(lái)那么簡(jiǎn)單,可能帶有一些復(fù)雜的操作驱敲,這種場(chǎng)景只用 slot 是不行的铁蹈,沒(méi)辦法確定是那一列的 slot。這種場(chǎng)景有兩種解決方案众眨,其一就是 Render 函數(shù)握牧,下一節(jié)的實(shí)戰(zhàn)就是開(kāi)發(fā)這樣一個(gè) Table 組件容诬;另一種是用作用域 slot(slot-scope),后面小節(jié)也會(huì)詳細(xì)介紹沿腰。
Functional Render
Vue.js 提供了一個(gè) functional
的布爾值選項(xiàng)览徒,設(shè)置為 true 可以使組件無(wú)狀態(tài)和無(wú)實(shí)例,也就是沒(méi)有 data 和 this 上下文矫俺。這樣用 Render 函數(shù)返回虛擬節(jié)點(diǎn)可以更容易渲染,因?yàn)楹瘮?shù)化組件(Functional Render)只是一個(gè)函數(shù)掸冤,渲染開(kāi)銷(xiāo)要小很多厘托。
使用函數(shù)化組件,Render 函數(shù)提供了第二個(gè)參數(shù) context 來(lái)提供臨時(shí)上下文稿湿。組件需要的 data铅匹、props、slots饺藤、children包斑、parent 都是通過(guò)這個(gè)上下文來(lái)傳遞的,比如 this.level 要改寫(xiě)為 context.props.level涕俗,this.$slots.default 改寫(xiě)為 context.children罗丰。
您可以閱讀 Vue.js 文檔—函數(shù)式組件 來(lái)查看示例。
函數(shù)化組件在業(yè)務(wù)中并不是很常用再姑,而且也有類(lèi)似的方法來(lái)實(shí)現(xiàn)萌抵,比如某些場(chǎng)景可以用 is 特性來(lái)動(dòng)態(tài)掛載組件。函數(shù)化組件主要適用于以下兩個(gè)場(chǎng)景:
- 程序化地在多個(gè)組件中選擇一個(gè)元镀;
- 在將 children绍填、props、data 傳遞給子組件之前操作它們栖疑。
比如上文說(shuō)過(guò)的讨永,某個(gè)組件需要使用 Render 函數(shù)來(lái)自定義,而不是通過(guò)傳遞普通文本或 v-html 指令遇革,這時(shí)就可以用 Functional Render卿闹,來(lái)看下面的示例:
-
首先創(chuàng)建一個(gè)函數(shù)化組件 render.js:
// render.js export default { functional: true, props: { render: Function }, render: (h, ctx) => { return ctx.props.render(h); } };
它只定義了一個(gè) props:render,格式為 Function萝快,因?yàn)槭?functional比原,所以在 render 里使用了第二個(gè)參數(shù)
ctx
來(lái)獲取 props。這是一個(gè)中間文件杠巡,并且可以復(fù)用量窘,其它組件需要這個(gè)功能時(shí),都可以引入它氢拥。 -
創(chuàng)建組件:
<!-- my-component.vue --> <template> <div> <Render :render="render"></Render> </div> </template> <script> import Render form './render.js'; export default { components: { Render }, props: { render: Function } } </script>
-
使用上面的 my-compoennt 組件:
<!-- demo.vue --> <template> <div> <my-component :render="render"></my-component> </div> </template> <script> import myComponent from '../components/my-component.vue'; export default { components: { myComponent }, data () { return { render: (h) => { return h('div', { style: { color: 'red' } }, '自定義內(nèi)容'); } } } } </script>
這里的 render.js 因?yàn)橹皇前?demo.vue 中的 Render 內(nèi)容過(guò)繼蚌铜,并無(wú)其它用處锨侯,所以用了 Functional Render。
就此例來(lái)說(shuō)冬殃,完全可以用 slot 取代 Functional Render囚痴,那是因?yàn)橹挥?render
這一個(gè) prop。如果示例中的 <Render>
是用 v-for
生成的审葬,也就是多個(gè)時(shí)深滚,用 一個(gè) slot 是實(shí)現(xiàn)不了的,那時(shí)用 Render 函數(shù)就很方便了涣觉,后面章節(jié)會(huì)專(zhuān)門(mén)介紹痴荐。
結(jié)語(yǔ)
如果想換一種思路寫(xiě) Vue.js,就試試 Render 函數(shù)吧官册,它會(huì)讓你“又愛(ài)又恨”生兆!
注:本節(jié)部分內(nèi)容參考了《Vue.js 實(shí)戰(zhàn)》(清華大學(xué)出版社),部分代碼參考 iView膝宁。