開門見山叉钥,我在codepen上寫了個簡單demo:上下兩個按鈕罢缸,功能一致(開關(guān));不同之處是:前者只改變文字投队,后者順帶改變了背景顏色枫疆。OK,怎么實現(xiàn)呢敷鸦?
實現(xiàn)相同功能的模塊息楔,理想方式自然是提供通用的抽象組件。這個例子中扒披,想要配合背景更換的效果值依,最直觀的設(shè)計應(yīng)該是實現(xiàn)一個子組件,再根據(jù)父組件傳遞的props值——比如一個叫backgroud的function——聯(lián)動更改背景色碟案。
不過鳞滨,在某些場景中,我們想要的是模塊提供固定的功能邏輯蟆淀,但并不希望它限制頁面的渲染(比如拯啦,我希望給開關(guān)加上更酷炫的動態(tài)效果,而不僅僅只有更換背景色這種單調(diào)的操作)熔任。組件設(shè)計者顯然不可能盡善盡美地提供所有候選props褒链,這時候留出更多的自定義空間反倒是一個比較切合實際的解決途徑。
Render函數(shù)
進(jìn)入正題前疑苔,先簡介一下Vue是如何渲染組件的甫匹。眾所周知,在工程中,我們會在.vue文件中定義<template>
兵迅、<script>
和<style>
三種tag抢韭,分別盛放組件html、javascript和css恍箭。
<template>
<button class="mood">
{{ state ? 'On' : 'Off' }}
</button>
</template>
<script>
export default {
data: () => ({ state: false })
}
</script>
<style>
.mood:after {
color: white;
background: blue;
}
</style>
但事實上刻恭,最后在生產(chǎn)環(huán)境中,我們只使用了一個巨大的JS文件——just JavaScript扯夭。究其緣由還是得益于webpack的vue loader鳍贾,它幫助我們把上述三部分提取出來,比如上述的.vue文件交洗,經(jīng)過vue loader后骑科,大體會成為如下這種樣式:
exprot default {
template: `<button class="mood">{{ state ? 'On' : 'Off' }}</button>`,
data: () => ({ state: false })
}
vuejs會把template元素提取出來,并進(jìn)一步編譯成一個叫render的函數(shù)构拳。(有關(guān)render函數(shù)可以參考官方文檔)
render(h) {
return h(
'button',
{class: 'mood'},
state ? 'On' : 'Off'
)
}
render函數(shù)最后會被vue優(yōu)化成VNode(虛節(jié)點)咆爽,具體過程我不再贅述了。不過置森,這里提供了一個很有趣的思路:編寫組件時伍掀,我們其實可以不寫vue文件,不寫template暇藏,只需要寫render函數(shù)蜜笤。
const button = {
render(h) {
return h(
'button',
{class: 'mood'},
state ? 'On' : 'Off'
)
},
data() {
return {state: false}
}
}
So? Renderless?
前提概要結(jié)束了,這里引入一個Renderless component的概念盐碱,直譯的話應(yīng)該叫非渲染組件把兔,國內(nèi)好多人喜歡叫它函數(shù)式組件。
Renderless意思就是組件只提供數(shù)據(jù)操作瓮顽,不渲染任何內(nèi)容县好。我們擱置爭議,只看非渲染組件的具體實現(xiàn)暖混。
const toggle = {
render() {
...
},
data() {
return { state: true }
},
methods: {
toggle() {
this.state = !this.state
}
}
}
new Vue({
el: '#parent',
components: { toggle },
...
})
toggle
就是所謂的Renderless組件了缕贡,只有數(shù)據(jù)和方法,不提供html template拣播。父組件直接將其放入components即可當(dāng)作一般子組件使用晾咪。
Slots in Renderless
那誰負(fù)責(zé)渲染工作呢?嗯贮配,就是Slots谍倦!父組件通過傳遞自定義的slots來定制子組件的html template。
<toggle v-slot:default="{on, toggle}">
<div class="container">
<button @click="click(toggle)">
{{on ? 'On' : 'Off'}}
</button>
</div>
</toggle>
這里提一下v-slot
泪勒,它是vue 2.6以后的新語法昼蛀,用來代替之前的slot
和slot-scope
宴猾;v-slot:default
還可以簡寫成#default
。Vue3應(yīng)該不會再保留slot
和slot-scope
這種不倫不類的標(biāo)簽了叼旋。
Scoped Slots
<toggle #default="{on, toggle}">
上文中用到了作用域插槽仇哆。這個例子中我希望能讓插槽訪問到子組件toggle
里的數(shù)據(jù)和方法,以便之后點擊button更改狀態(tài)夫植。子組件暴露作用域插槽也很簡單讹剔,只要在render函數(shù)里返回$scopedSlots
對象即可,這里因方便起見使用了默認(rèn)的default
插槽偷崩,自己實現(xiàn)的時候也可以重命名為任意插槽辟拷。
//toggle.js
const toggle = {
render() {
return this.$scopedSlots.default({
on: this.state,
toggle: this.toggle,
})
},
data() {
return { state: true }
},
methods: {
toggle() {
this.state = !this.state
}
}
}
Using toggle component
最后我們在父組件調(diào)用renderless組件:
<template>
<toggle v-slot="{on, toggle}">
<div class="container">
<button @click="click(toggle)">
{{on ? 'On' : 'Off'}}
</button>
</div>
</toggle>
</template>
<script>
import toggle from 'toggle';
export default {
components: { toggle },
methods: {
click(fn) {
fn()
},
},
}
</script>
這樣一個簡單的renderless開關(guān)就實現(xiàn)了撞羽,
Customized Component
假如你想自定義組件樣式阐斜,或是說控制toggle渲染方式,更改也很容易诀紊,只需要在插槽里寫下自定義代碼即可:
<toggle #default="{ on, toggle }">
<div class="container">
<button @click="click(toggle)"
:style="{background: on ? 'green' : 'red'}">
{{on ? 'On' : 'Off'}}
</button>
</div>
</toggle>
因為toggle的邏輯不變谒出,所以我們不需要更改這個renderless組件。只需稍微改動一下slot邻奠,button的背景色就會隨著開關(guān)一齊改變了笤喳。嗯,這就是Renderless組件的效果碌宴,功能邏輯和頁面渲染分開杀狡。
更炫酷的開關(guān)就由大家來完成吧。
小結(jié)
這期用一個很簡單的例子科普了Renderless Component贰镣。所謂Renderless就是利用render函數(shù)和slot呜象,將組件的功能邏輯與前端渲染分離開來,這種設(shè)計更符合傳統(tǒng)軟件工程的單一職責(zé)和開放閉合原則碑隆。當(dāng)然這和VUE設(shè)計之初的理念并不相符恭陡,vue作者似乎并不屑于這種形式,我在尤雨溪的某些文章里還看到他噴renderless嘩眾取寵上煤,帶來無謂的的性能開銷休玩。
我自己倒是挺贊許renderless的。在工程開發(fā)中確實會碰到了功能邏輯相似劫狠,但樣式表現(xiàn)不一的組件簇拴疤;通過將底層邏輯以renderless子組件的形式封裝起來,可以很好地實現(xiàn)代碼復(fù)用的目標(biāo)《琅ⅲ現(xiàn)實開發(fā)中遥赚,具體情況還是要具體分析滴。