在Vue中使用HOC模式的實現(xiàn)
前言
HOC是React常用的一種模式,但HOC只能是在React才能玩嗎?先來看看React官方文檔是怎么介紹HOC的:
高階組件(HOC)是React中用于復用組件邏輯的一種高級技巧。HOC自身不是ReactAPI的一部分,它是一種基于React的組合特性而形成的設計模式。
HOC它是一個模式,是一種思想,并不是只能在React中才能用。所以結(jié)合Vue的特性,一樣能在Vue中玩HOC。
HOC
HOC要解決的問題
并不是說哪種技術新穎,就得使用哪一種。得看這種技術能夠解決哪些痛點。
HOC主要解決的是可復用性的問題。在Vue中,這種問題一般是用Mixin解決的。Mixin是一種通過擴展收集功能的方式,它本質(zhì)上是將一個對象的屬性拷貝到另一個對象上去。
最初React也是使用Mixin的,但是后面發(fā)現(xiàn)Mixin在React中并不是一種好的模式,它有以下的缺點:
- mixin與組件之間容易導致命名沖突
- mixin是侵入式的,改變了原組件,復雜性大大提高。
所以React就慢慢的脫離了mixin,從而推薦使用HOC。并不是mixin不優(yōu)秀,只是mixin不適合React。
HOC是什么
HOC全稱:high-order component--也就是高階組件。具體而言,高階組件是參數(shù)為組件,返回值為新組件的函數(shù)。
而在React和Vue中組件就是函數(shù),所以的高階組件其實就是高階函數(shù),也就是返回一個函數(shù)的函數(shù)。
來看看HOC在React的用法:
function withComponent(WrappedComponent) {
return class extends Component {
componentDidMount () {
console.log('已經(jīng)掛載完成')
}
render() {
return <WrappedComponent {...props} />;
}
}
}
withComponent就是一個高階組件,它有以下特點:
- HOC是一個純函數(shù),且不應該修改原組件
- HOC不關心傳遞的props是什么,并且WrappedComponent不關心數(shù)據(jù)來源
- HOC接收到的props應該透傳給WrapperComponent
在Vue中使用HOC
怎么樣才能將Vue上使用HOC的模式呢?
我們一般書寫的Vue組件是這樣的:
<template>
<div>
<p>{{title}}</p>
<button @click="changeTitle"></button>
</div>
</template>
<script>
export default {
name: 'ChildComponent',
props: ['title'],
methods: {
changeTitle () {
this.$emit('changeTitle');
}
}
}
</script>
而withComponet函數(shù)的功能是在每次掛載完成后都打印一句:已經(jīng)掛載完成。
既然HOC是替代mixin的,所以我們先用mixin書寫一遍:
export default {
mounted () {
console.log('已經(jīng)掛載完成')
}
}
然后導入到ChildComponent中
import withComponent from './withComponent';
export default {
...
mixins: ['withComponet'],
}
對于這個組件,我們在父組件中是這樣調(diào)用的
<child-component :title='title' @changeTitle='changeTitle'></child-component>
<script>
import ChildComponent from './childComponent.vue';
export default {
...
components: {ChildComponent}
}
</script>
大家有沒有發(fā)現(xiàn),當我們導入一個Vue組件時,其實是導入一個對象。
export default {}
至于說組件是函數(shù),其實是經(jīng)過處理之后的結(jié)果。所以Vue中的高階組件也可以是:接收一個純對象,返回一個純對象。
所以改為HOC模式,是這樣的:
export default function withComponent (WrapperComponent) {
return {
mounted () {
console.log('已經(jīng)掛載完成')
},
props: WrappedComponent.props,
render (h) {
return h(WrapperComponent, {
on: this.$listeners,
attrs: this.$attrs,
props: this.$props
})
}
}
}
注意{on: this.$listeners,attr: this.$attrs, props: this.props}這一句就是透傳props的原理,等價于React中的<WrappedComponent {...props} />;
this.$props是指已經(jīng)被聲明的props屬性,this.$attrs是指沒被聲明的props屬性。這一定要兩個一起透傳,缺少哪一個,props都不完整。
為了通用性,這里使用了render函數(shù)來構(gòu)建,這是因為template只有在完整版的Vue中才能使用。
這樣似乎還不錯,但是還有一個重要的問題,在Vue組件中是可以使用插槽的。
比如:
<template>
<div>
<p>{{title}}</p>
<button @click="changeTitle"></button>
<slot></slot>
</div>
</template>
在父組件中
<child-component :title='title' @changeTitle='changeTitle'>Hello, HOC</child-component>
可以用this.$solts訪問到被插槽分發(fā)的內(nèi)容。每個具名插槽都有其相應的property,例如v-slot:foo中的內(nèi)容將會在this.$slots.foo中被找到。而default property包括了所有沒有被包含在具名插槽中的節(jié)點,或v-slot:default的內(nèi)容。
所以在使用渲染函數(shù)書寫一個組件時,訪問this.$slots最有幫助的。
先將this.$slots轉(zhuǎn)化為數(shù)組,因為渲染函數(shù)的第三個參數(shù)是子節(jié)點,是一個數(shù)組
export default function withComponent (WrapperComponent) {
return {
mounted () {
console.log('已經(jīng)掛載完成')
},
props: WrappedComponent.props,
render (h) {
const keys = Object.keys(this.$slots);
const slotList = keys.reduce((arr, key) => arr.concat(this.$slots[key]), []);
return h(WrapperComponent, {
on: this.$listeners,
attrs: this.$attrs,
props: this.$props
}, slotList)
}
}
}
總算是有模有樣了,但這還沒結(jié)束,你會發(fā)現(xiàn)使不使用具名插槽都一樣,最后都是按默認插槽來處理的。
有點納悶,去看看Vue源碼中是怎么具名插槽的。
在src/core/instance/render.js文件中找到了initRender函數(shù),在初始化render函數(shù)時
const options = vm.$options const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree const renderContext = parentVnode && parentVnode.context vm.$slots = resolveSlots(options._renderChildren, renderContext)
這一段代碼是Vue解析并處理slot的。
將vm.$options._parentVnode賦值為vm.$vnode,也就是$vnode就是父組件的vnode。如果父組件存在,定義renderContext = vm.$vnode.context。renderContext就是父組件要渲染的實例。 然后把renderContext和$options._renderChildren作為參數(shù)傳進resolveSlots()函數(shù)中。
接下里看看resolveSlots()函數(shù),在src/core/instance/render-helper/resolve-slots.js文件中
export function resolveSlots (
children: ?Array<VNode>,
context: ?Component
): { [key: string]: Array<VNode> } {
if (!children || !children.length) {
return {}
}
const slots = {}
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]
const data = child.data
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
const name = data.slot
const slot = (slots[name] || (slots[name] = []))
if (child.tag === 'template') {
slot.push.apply(slot, child.children || [])
} else {
slot.push(child)
}
} else {
(slots.default || (slots.default = [])).push(child)
}
}
// ignore slots that contains only whitespace
for (const name in slots) {
if (slots[name].every(isWhitespace)) {
delete slots[name]
}
}
return slots
}
重點來看里面的一段if語句
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
const name = data.slot
const slot = (slots[name] || (slots[name] = []))
if (child.tag === 'template') {
slot.push.apply(slot, child.children || [])
} else {
slot.push(child)
}
} else {
(slots.default || (slots.default = [])).push(child)
}
只有當if ((child.context === context || child.fnContext === context) && data && data.slot != null ) 為真時,才處理為具名插槽,否則不管具名不具名,都當成默認插槽處理
else {
(slots.default || (slots.default = [])).push(child)
}
那為什么HOC上的if條件是不成立的呢?
這是因為由于HOC的介入,在原本的父組件與子組件之間插入了一個組件--也就是HOC,這導致了子組件中訪問的this.$vode已經(jīng)不是原本的父組件的vnode了,而是HOC中的vnode,所以這時的this.$vnode.context引用的是高階組件,但是我們卻將slot透傳了,slot中的VNode的context引用的還是原來的父組件實例,所以就導致不成立。
從而都被處理為默認插槽。
解決方法也很簡單,只需手動的將slot中的vnode的context指向為HOC實例即可。注意當前實例 _self 屬性訪問當前實例本身,而不是直接使用 this,因為 this 是一個代理對象。
export default function withComponent (WrapperComponent) {
return {
mounted () {
console.log('已經(jīng)掛載完成')
},
props: WrappedComponent.props,
render (h) {
const keys = Object.keys(this.$slots);
const slotList = keys.reduce((arr, key) => arr.concat(this.$slots[key]), []).map(vnode => {
vnode.context = this._self
return vnode
});
return h(WrapperComponent, {
on: this.$listeners,
attrs: this.$attrs,
props: this.$props
}, slotList)
}
}
}
而且scopeSlot與slot的處理方式是不同的,所以將scopeSlot一起透傳
export default function withComponent (WrapperComponent) {
return {
mounted () {
console.log('已經(jīng)掛載完成')
},
props: WrappedComponent.props,
render (h) {
const keys = Object.keys(this.$slots);
const slotList = keys.reduce((arr, key) => arr.concat(this.$slots[key]), []).map(vnode => {
vnode.context = this._self
return vnode
});
return h(WrapperComponent, {
on: this.$listeners,
attrs: this.$attrs,
props: this.$props,
scopedSlots: this.$scopedSlots
}, slotList)
}
}
}
這樣就行了。
結(jié)尾
更多文章請移步樓主github,如果喜歡請點一下star,對作者也是一種鼓勵。
到此這篇關于在Vue中使用HOC模式的文章就介紹到這了,更多相關Vue使用HOC模式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue+elementui實現(xiàn)動態(tài)控制表格列的顯示和隱藏
這篇文章主要介紹了vue+elementui實現(xiàn)動態(tài)控制表格列的顯示和隱藏,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04
vue實現(xiàn)div可拖動位置也可改變盒子大小的原理
這篇文章主要介紹了vue實現(xiàn)div可拖動位置也可改變盒子大小,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
vue中axios的封裝問題(簡易版攔截,get,post)
這篇文章主要介紹了vue中axios的封裝問題(簡易版攔截,get,post),需要的朋友可以參考下2018-06-06
使用Vue.js實現(xiàn)一個循環(huán)倒計時功能
在Web應用中,倒計時功能常用于各種場景,如活動倒計時、定時任務提醒等,Vue.js作為一款輕量級的前端框架,提供了豐富的工具和API來實現(xiàn)這些功能,本文將詳細介紹如何使用Vue.js實現(xiàn)一個循環(huán)倒計時功能,需要的朋友可以參考下2024-09-09

