1. 程式人生 > 程式設計 >在Vue中使用HOC模式的實現

在Vue中使用HOC模式的實現

前言

HOC是React常用的一種模式,但HOC只能是在React才能玩嗎?先來看看React官方文件是怎麼介紹HOC的:

高階元件(HOC)是React中用於複用元件邏輯的一種高階技巧。HOC自身不是ReactAPI的一部分,它是一種基於React的組合特性而形成的設計模式。

HOC它是一個模式,是一種思想,並不是只能在React中才能用。所以結合Vue的特性,一樣能在Vue中玩HOC。

HOC

HOC要解決的問題

並不是說哪種技術新穎,就得使用哪一種。得看這種技術能夠解決哪些痛點。

HOC主要解決的是可複用性的問題。在Vue中,這種問題一般是用Mixin解決的。Mixin是一種通過擴充套件收集功能的方式,它本質上是將一個物件的屬性拷貝到另一個物件上去。

最初React也是使用Mixin的,但是後面發現Mixin在React中並不是一種好的模式,它有以下的缺點:

  • mixin與元件之間容易導致命名衝突
  • mixin是侵入式的,改變了原元件,複雜性大大提高。

所以React就慢慢的脫離了mixin,從而推薦使用HOC。並不是mixin不優秀,只是mixin不適合React。

HOC是什麼

HOC全稱:high-order component--也就是高階元件。具體而言,高階元件是引數為元件,返回值為新元件的函式。

而在React和Vue中元件就是函式,所以的高階元件其實就是高階函式,也就是返回一個函式的函式。

來看看HOC在React的用法:

function withComponent(WrappedComponent) {
  return class extends Component {
    componentDidMount () {
      console.log('已經掛載完成')
    }
    render() {
      return <WrappedComponent {...props} />;
    }
  }
}

withComponent就是一個高階元件,它有以下特點:

  • HOC是一個純函式,且不應該修改原元件
  • HOC不關心傳遞的props是什麼,並且WrappedComponent不關心資料來源
  • 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函式的功能是在每次掛載完成後都列印一句:已經掛載完成。

既然HOC是替代mixin的,所以我們先用mixin書寫一遍:

export default {
  mounted () {
    console.log('已經掛載完成')
  }
}

然後匯入到ChildComponent中

import withComponent from './withComponent';
export default {
  ...
  mixins: ['withComponet'],}

對於這個元件,我們在父元件中是這樣呼叫的

<child-component :title='title' @changeTitle='changeTitle'></child-component>

<script>
import ChildComponent from './childComponent.vue';
export default {
  ...
  components: {ChildComponent}
}
</script>

大家有沒有發現,當我們匯入一個Vue元件時,其實是匯入一個物件。

export default {}

至於說元件是函式,其實是經過處理之後的結果。所以Vue中的高階元件也可以是:接收一個純物件,返回一個純物件。

所以改為HOC模式,是這樣的:

export default function withComponent (WrapperComponent) {
  return {
    mounted () {
      console.log('已經掛載完成')
    },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是指已經被宣告的props屬性,this.$attrs是指沒被宣告的props屬性。這一定要兩個一起透傳,缺少哪一個,props都不完整。

為了通用性,這裡使用了render函式來構建,這是因為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訪問到被插槽分發的內容。每個具名插槽都有其相應的property,例如v-slot:foo中的內容將會在this.$slots.foo中被找到。而default property包括了所有沒有被包含在具名插槽中的節點,或v-slot:default的內容。

所以在使用渲染函式書寫一個元件時,訪問this.$slots最有幫助的。

先將this.$slots轉化為陣列,因為渲染函式的第三個引數是子節點,是一個數組

export default function withComponent (WrapperComponent) {
  return {
    mounted () {
         console.log('已經掛載完成')
    },render (h) {
      const keys = Object.keys(this.$slots);
      const slotList = keys.reduce((arr,key) => arr.concat(this.$slots[key]),[]);
      return h(WrapperComponent,props: this.$props
      },slotList)
    }
  }
}

總算是有模有樣了,但這還沒結束,你會發現使不使用具名插槽都一樣,最後都是按預設插槽來處理的。

有點納悶,去看看Vue原始碼中是怎麼具名插槽的。
在src/core/instance/render.js檔案中找到了initRender函式,在初始化render函式時

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作為引數傳進resolveSlots()函式中。

接下里看看resolveSlots()函式,在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已經不是原本的父元件的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('已經掛載完成')
    },[]).map(vnode => {
        vnode.context = this._self
        return vnode
      });
      return h(WrapperComponent,slotList)
    }
  }
}

而且scopeSlot與slot的處理方式是不同的,所以將scopeSlot一起透傳

export default function withComponent (WrapperComponent) {
  return {
    mounted () {
         console.log('已經掛載完成')
    },props: this.$props,scopedSlots: this.$scopedSlots
      },slotList)
    }
  }
}

這樣就行了。

結尾

更多文章請移步樓主github,如果喜歡請點一下star,對作者也是一種鼓勵。

到此這篇關於在Vue中使用HOC模式的文章就介紹到這了,更多相關Vue使用HOC模式內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!