您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)如何在Vue項(xiàng)目中使用HOC模式,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。
前言
HOC是React常用的一種模式,但HOC只能是在React才能玩嗎?先來(lái)看看React官方文檔是怎么介紹HOC的:
高階組件(HOC)是React中用于復(fù)用組件邏輯的一種高級(jí)技巧。HOC自身不是ReactAPI的一部分,它是一種基于React的組合特性而形成的設(shè)計(jì)模式。
HOC它是一個(gè)模式,是一種思想,并不是只能在React中才能用。所以結(jié)合Vue的特性,一樣能在Vue中玩HOC。
HOC
HOC要解決的問(wèn)題
并不是說(shuō)哪種技術(shù)新穎,就得使用哪一種。得看這種技術(shù)能夠解決哪些痛點(diǎn)。
HOC主要解決的是可復(fù)用性的問(wèn)題。在Vue中,這種問(wèn)題一般是用Mixin解決的。Mixin是一種通過(guò)擴(kuò)展收集功能的方式,它本質(zhì)上是將一個(gè)對(duì)象的屬性拷貝到另一個(gè)對(duì)象上去。
最初React也是使用Mixin的,但是后面發(fā)現(xiàn)Mixin在React中并不是一種好的模式,它有以下的缺點(diǎn):
所以React就慢慢的脫離了mixin,從而推薦使用HOC。并不是mixin不優(yōu)秀,只是mixin不適合React。
HOC是什么
HOC全稱(chēng):high-order component--也就是高階組件。具體而言,高階組件是參數(shù)為組件,返回值為新組件的函數(shù)。
而在React和Vue中組件就是函數(shù),所以的高階組件其實(shí)就是高階函數(shù),也就是返回一個(gè)函數(shù)的函數(shù)。
來(lái)看看HOC在React的用法:
function withComponent(WrappedComponent) { return class extends Component { componentDidMount () { console.log('已經(jīng)掛載完成') } render() { return <WrappedComponent {...props} />; } } }
withComponent就是一個(gè)高階組件,它有以下特點(diǎn):
在Vue中使用HOC
怎么樣才能將Vue上使用HOC的模式呢?
我們一般書(shū)寫(xiě)的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書(shū)寫(xiě)一遍:
export default { mounted () { console.log('已經(jīng)掛載完成') } }
然后導(dǎo)入到ChildComponent中
import withComponent from './withComponent'; export default { ... mixins: ['withComponet'], }
對(duì)于這個(gè)組件,我們?cè)诟附M件中是這樣調(diào)用的
<child-component :title='title' @changeTitle='changeTitle'></child-component> <script> import ChildComponent from './childComponent.vue'; export default { ... components: {ChildComponent} } </script>
大家有沒(méi)有發(fā)現(xiàn),當(dāng)我們導(dǎo)入一個(gè)Vue組件時(shí),其實(shí)是導(dǎo)入一個(gè)對(duì)象。
export default {}
至于說(shuō)組件是函數(shù),其實(shí)是經(jīng)過(guò)處理之后的結(jié)果。所以Vue中的高階組件也可以是:接收一個(gè)純對(duì)象,返回一個(gè)純對(duì)象。
所以改為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}
這一句就是透?jìng)鱬rops的原理,等價(jià)于React中的<WrappedComponent {...props} />;
this.$props是指已經(jīng)被聲明的props屬性,this.$attrs是指沒(méi)被聲明的props屬性。這一定要兩個(gè)一起透?jìng)?,缺少哪一個(gè),props都不完整。
為了通用性,這里使用了render函數(shù)來(lái)構(gòu)建,這是因?yàn)閠emplate只有在完整版的Vue中才能使用。
這樣似乎還不錯(cuò),但是還有一個(gè)重要的問(wèn)題,在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訪問(wèn)到被插槽分發(fā)的內(nèi)容。每個(gè)具名插槽都有其相應(yīng)的property,例如v-slot:foo中的內(nèi)容將會(huì)在this.$slots.foo中被找到。而default property包括了所有沒(méi)有被包含在具名插槽中的節(jié)點(diǎn),或v-slot:default的內(nèi)容。
所以在使用渲染函數(shù)書(shū)寫(xiě)一個(gè)組件時(shí),訪問(wèn)this.$slots最有幫助的。
先將this.$slots轉(zhuǎn)化為數(shù)組,因?yàn)殇秩竞瘮?shù)的第三個(gè)參數(shù)是子節(jié)點(diǎn),是一個(gè)數(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) } } }
總算是有模有樣了,但這還沒(méi)結(jié)束,你會(huì)發(fā)現(xiàn)使不使用具名插槽都一樣,最后都是按默認(rèn)插槽來(lái)處理的。
有點(diǎn)納悶,去看看Vue源碼中是怎么具名插槽的。
在src/core/instance/render.js文件中找到了initRender函數(shù),在初始化render函數(shù)時(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就是父組件要渲染的實(shí)例。 然后把renderContext和$options._renderChildren作為參數(shù)傳進(jìn)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 }
重點(diǎn)來(lái)看里面的一段if語(yǔ)句
// 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) }
只有當(dāng)if ((child.context === context || child.fnContext === context) && data && data.slot != null ) 為真時(shí),才處理為具名插槽,否則不管具名不具名,都當(dāng)成默認(rèn)插槽處理
else { (slots.default || (slots.default = [])).push(child) }
那為什么HOC上的if條件是不成立的呢?
這是因?yàn)橛捎贖OC的介入,在原本的父組件與子組件之間插入了一個(gè)組件--也就是HOC,這導(dǎo)致了子組件中訪問(wèn)的this.$vode已經(jīng)不是原本的父組件的vnode了,而是HOC中的vnode,所以這時(shí)的this.$vnode.context引用的是高階組件,但是我們卻將slot透?jìng)髁?,slot中的VNode的context引用的還是原來(lái)的父組件實(shí)例,所以就導(dǎo)致不成立。
從而都被處理為默認(rèn)插槽。
解決方法也很簡(jiǎn)單,只需手動(dòng)的將slot中的vnode的context指向?yàn)镠OC實(shí)例即可。注意當(dāng)前實(shí)例 _self 屬性訪問(wèn)當(dāng)前實(shí)例本身,而不是直接使用 this,因?yàn)?this 是一個(gè)代理對(duì)象。
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一起透?jìng)?/p>
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) } } }
關(guān)于如何在Vue項(xiàng)目中使用HOC模式就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。