react與vue中高階組件的對比

由高階函數(shù)引申出來的高階組件

高階組件本質(zhì)上也是一個(gè)函數(shù),并不是一個(gè)組件,且高階組件是一個(gè)純函數(shù)。
高階組件,顧名思義,就是傳入一個(gè)組件,輸出一個(gè)組件。

1 React中的高階組件

一個(gè)最簡單的例子:

// 高階組件
import React,{Component} from 'react';

export default function withHeader(WrappedComponent) {
  return class HOC extends Component{
    render() {
      return <div>
        <div className="demo-header">
          我是高階組件的標(biāo)題
        </div>
        <WrappedComponent {...this.props} title={'我是高階組件過來的title'} />
      </div>
    }
  }
}

// 使用高階組件
import React,{Component} from 'react';
import withHeader from "./withHeader";
class HighDemo extends Component {
  render() {
    return (
      <div>
        我是一個(gè)普通組件A
        <div>{this.props.title}</div>
      </div>
    );
  }
}
export default withHeader(HighDemo)

結(jié)果展示:


image.png

以上是一個(gè)簡單的例子,但并沒辦法體現(xiàn)出高階組件的好處。

高階組件的實(shí)現(xiàn)方式有兩種:屬性代理以及反向繼承

1.1 屬性代理

把變的部分(組件和獲取數(shù)據(jù)的方法) 抽離到外部作為傳入,從而實(shí)現(xiàn)頁面的復(fù)用

應(yīng)用場景:不同類型的地址列表展示

傳統(tǒng)方式:先定義一個(gè)可復(fù)用的組件,再根據(jù)不同類型查詢數(shù)據(jù)再引用

import React,{Component} from 'react';
import addressApi from '../api/address'
import AddressList from "../components/AddressList";

class TraditionWay extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data:[],
      addressType:'收貨地址',

    }
  }

  getAddressData=async ()=>{
    const data = await addressApi.fetchCompanyAddresses();
    this.setState({
      data:data.data
    });
  }

  componentDidMount() {
    this.getAddressData()
  }

  render() {
    if(this.state.data.length) {
      return (
        <AddressList {...this.state} editClick={addressApi.editClick}/>
      )
    }
    return (
      <div>暫無數(shù)據(jù)</div>
    )
  }

}
export default TraditionWay

在這里,多個(gè)類型的話,就需要寫多個(gè)類似的頁面。數(shù)據(jù)查詢與組件引用均需寫多次,顯得代碼有些重復(fù)。
如果我們使用高階組件,則可以這樣實(shí)現(xiàn)

// 高階組件
import React from 'react';
const AddressHOC = ({WrappedComponent, fetchingMethod, defaultProps,editClick}) => {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        data:[]
      }
    }
    async componentDidMount() {
      const data = await fetchingMethod();
      this.setState({
        data:data.data
      });
    }

    render() {
      if(this.state.data && this.state.data.length){
        return (
          <WrappedComponent
            data={this.state.data}
            {...defaultProps}
            {...this.props}
            editClick={editClick}
          />
        );
      }
      return (<div>{defaultProps.emptyTips}</div>)
    }
  }
}

export default AddressHOC


// 收貨地址頁面
import AddressHOC from '../components/AddressHOC';
import addressApi from '../api/address';
import AddressList from '../components/AddressList';
const defaultProps = {emptyTips: '暫無收貨地址',addressType:'收貨'}

export default AddressHOC({WrappedComponent:AddressList, fetchingMethod:addressApi.fetchCompanyAddresses, defaultProps,editClick:addressApi.editClick});


// 寄票地址頁面
import AddressHOC from '../components/AddressHOC';
import addressApi from '../api/address';
import AddressList from '../components/AddressList';
const defaultProps = {emptyTips: '暫無寄票地址',addressType:'寄票'}

export default AddressHOC({WrappedComponent:AddressList, fetchingMethod:addressApi.fetchCompanyMailAddresses, defaultProps,editClick:addressApi.editClick});

顯然,代碼沒有那么冗余,也方便了復(fù)用。


image.png
1.2 反向繼承

高階組件可以通過this直接訪問原組件的state/ref/生命周期方法,
作用:劫持渲染、操作state

主要是調(diào)用

super.render()

一個(gè)簡單的例子是:

// 原組件
import React,{Component} from 'react';

class ButtonItem extends Component{
  constructor(props) {
    super(props);
    this.state = {
      title: 'buttonTitle'
    }
  }
  clickComponent(){
    console.log('按鈕點(diǎn)擊')
  }
  render() {
    return <button onClick={this.clickComponent}>{this.state.title}</button>
  }
}

export default ButtonItem

不重寫state跟clickComponent的情況下,高階組件能直接使用this訪問原組件的方法跟state

// 反向繼承高階組件
import React from 'react';

const ButtonHOC = (WrappedComponent)=>  class extends WrappedComponent {
  render(){
    return (
      <div>
        <div onClick={this.clickComponent}>ButtonHOC 點(diǎn)擊</div>
        <div>{super.render()}</div>
      </div>
    )
  }
}

export default ButtonHOC
image.png

重寫state跟clickComponent的情況下,原組件的state與方法都會(huì)被覆蓋

// 反向繼承高階組件
import React from 'react';

const ButtonHOC = (WrappedComponent)=>  class extends WrappedComponent {
  constructor(props) {
    super(props);
    this.state = {
      title: 'HOC繼承'
    }
  }
  clickComponent(){
    console.log('HOC繼承點(diǎn)擊')
  }
  render(){
    return (
      <div>
        <div onClick={this.clickComponent}>ButtonHOC 點(diǎn)擊</div>
        <div>{super.render()}</div>
      </div>
    )
  }
}

export default ButtonHOC
image.png

使用場景:跟蹤組件性能

/**
 * 反向繼承 跟蹤組件性能
 */
import  React,{Component} from 'react';

class Children extends Component {
  render() {
    return <h1>被反向繼承的組件</h1>
  }
}

function withTiming(WrappedComponent) {
  return class withTimingHOC extends WrappedComponent {
    constructor(props) {
      super(props);
      this.start = 0;
      this.end = 0;
    }

    componentWillMount() {
      super.componentWillMount && super.componentWillMount();
      this.start = Date.now();
    }

    componentDidMount() {
      super.componentDidMount && super.componentDidMount();
      this.end = Date.now();
      console.log(`${WrappedComponent.name} 組件渲染時(shí)間為 ${this.end - this.start} ms`);
    }

    render() {
      return super.render();
    }
  }
}

export default withTiming(Children)

image.png

React高階組件demo代碼地址

2 Vue2.0 中的高階組件

由于Vue官方不怎么推崇HOC,而且Mixins本身就能實(shí)現(xiàn)HOC的相關(guān)功能,所以Vue中對HOC的支持并不是很好。但我們還是可以強(qiáng)行在Vue中使用HOC,看看他到底怎樣。

// 封裝高階函數(shù)
import Vue from 'Vue'
import addressData from '../api/address'

const HOCAddress = ({component, fetchDataName, addressType}) => {
  return Vue.component('HocComponent', {
    render (createElement, hack) {
      return createElement(component, {
        props: {
          addressData: this.returnedData,
          addressType: this.addressType
        },
        on: { ...this.$listeners }
      })
    },
    data () {
      return {
        returnedData: [],
        addressType: addressType
      }
    },
    async created () {
      const data = await addressData[fetchDataName]()
      this.returnedData = data.data````
    }
  })
}

export default HOCAddress

// 高階函數(shù)的使用
<template>
  <AddressComponent @click="editAddress"></AddressComponent>
</template>

<script>
import HOCAddress from '../components/HOCAddress'
import AddressList from '../components/AddressList'

const AddressComponent = HOCAddress({component: AddressList,
  addressType: '辦公',
  fetchDataName: 'fetchTypeAddresses' })
export default {
  name: 'OfficeAddress',
  components: {
    AddressComponent
  },
  methods: {
    editAddress (value) {
      console.log(value)
    }
  }
}
</script>

<style scoped>

</style>

image.png

3.vue3 中的高階組件

這里將通過兩種方式實(shí)現(xiàn)所謂的高階組件,用到的知識(shí)點(diǎn)主要是Vue3中的具名插槽以及組合式 Api,公共自組件我們暫時(shí)不說,這里直接進(jìn)入主題,看看如何寫出一個(gè)可復(fù)用的高階組件

1.是使用具名插槽以及組合式API實(shí)現(xiàn)的組件

// fetch.vue
// 定義具名插槽
<template>
  <div>
  // 數(shù)據(jù)
    <slot v-if="data&&!loading" :data="data"/>
  // 分頁
    <slot v-if="!loading" name="pagination" v-bind="{ nextPage, prevPage,currentPage }"/>
  // loading
    <slot v-if="loading" name="loading"/>
  </div>
</template>
<script>
// 引用封裝好的函數(shù)
  import { useFetch, usePagination } from "../composables/HocFetch";
  import { toRefs } from "vue";

  export default {
    name: "Fetch",
    props: {
      paginate: Boolean,
      endpoint: String
    },
    setup(props) {
    // 使用toRefs的作用是:直接解構(gòu)props會(huì)使props失去響應(yīng)性,所以這里需要使用toRefs
      let { paginate, endpoint } = toRefs(props)
      let addonAPI = {};
      const pagination = usePagination();
      let currentPage = pagination.currentPage || 1
      if (paginate) {
        addonAPI = {
          ...addonAPI,
          currentPage:pagination.currentPage,
          nextPage: pagination.nextPage,
          prevPage: pagination.prevPage
        };

      }

      const coreAPI = useFetch(endpoint,currentPage);

      return {
        ...addonAPI,
        ...coreAPI
      };
    }
  };
</script>

// HocFetch.js
// 這里將分頁跟查詢分離,通過監(jiān)聽頁數(shù)變化查詢數(shù)據(jù)
import { ref, onMounted, isRef, watch } from "vue";
import address from "../api/address";

export function usePagination() {
  const currentPage = ref(1)
  function nextPage() {
    currentPage.value=++currentPage.value;
  }

  function prevPage() {
    if (currentPage.value <= 1) {
      return;
    }
    currentPage.value=--currentPage.value;
  }

  return {
    nextPage,
    prevPage,
    currentPage
  };
}

export function useFetch(endpoint,currentPage) {
  const data = ref(null);
  const loading = ref(true);

  function fetchData() {
    loading.value = true;
    setTimeout(async()=>{
      const addressList = await address[endpoint.value]({currentPage:currentPage.value})
      data.value = addressList.data
      loading.value = false;
    },1000)
  }

  onMounted(() => {
    fetchData();
  });

  if (isRef(currentPage)) {
    watch(currentPage, () => {
      fetchData();
    });
  }

  return {
    data,
    loading
  };
}

使用該組件

<template>
  <Fetch endpoint="fetchCompanyAddresses" paginate>
    <template #default="{ data }">
      <AddressList :addressData="data" :address-type="'辦公'" :editAddress="editAddress"></AddressList>
    </template>

    <template #loading>Loading....</template>

    <template #pagination="{ nextPage, prevPage,currentPage }">
      <div class="pagination">
        <div>當(dāng)前是第{{currentPage}}頁</div>
        <button @click="prevPage">上一頁</button>
        <button @click="nextPage">下一頁</button>
      </div>
    </template>
  </Fetch>
</template>

<script>
  import Fetch from "../components/Fetch.vue";
  import AddressList from "../components/AddressList.vue";
  import address from "../api/address";
  export default {
    name: "FetchIndex",
    data(){
      return {
        editAddress:address.editClick
      }
    },
    components:{
      Fetch,
      AddressList
    }
  }
</script>

<style scoped>

</style>

總結(jié):可復(fù)用且可拓展性高,但使用不方便,代碼不夠簡潔。

使用組合式Api以及函數(shù)式組件實(shí)現(xiàn)高階組件,利用setup中return component的功能實(shí)現(xiàn)

// AddressHocComponent.js
import {h, ref, onMounted, watch, isRef} from 'vue'
export default function HocComponent({WrappedComponent,fetchData,props}) {
  const addressData = ref([])
  const currentPage = ref(1)
  const loading = ref(true);
  const getData =async ()=>{
    loading.value = true
    setTimeout(async()=>{
      const data = await fetchData()
      addressData.value = data.data
      loading.value = false
    },1000)
  }
  function nextPage() {
    currentPage.value=++currentPage.value;
    getData()
  }

  function prevPage() {
    if (currentPage.value <= 1) {
      return;
    }
    currentPage.value=--currentPage.value;
    getData()
  }
  onMounted(()=>{
    getData()
  })
  const component= () =>{
    if(addressData.value&&addressData.value.length &&!loading.value){
      return h('div',[
        h(WrappedComponent,{...props,addressData:addressData.value}),
        h('div',[
          h('div',`當(dāng)前是第${currentPage.value}頁`),
          h('button',{ class: 'btn', onClick: prevPage }, '上一頁'),
          h('button',{ class: 'btn', onClick: nextPage }, '下一頁')
        ])
      ])
    }
    return h('div',{},'Loading...')
  }
  return {addressData, component,loading}
}

使用該高階組件

// companyMailAddressIndex.vue
<template></template>

<script>
  import AddressHocComponent from "../composables/AddressHocComponent";
  import AddressList from "../components/AddressList.vue";
  import address from "../api/address";
  export default {
    name: "companyMailAddressIndex",
    setup(props, context) {
      const { component } =
        AddressHocComponent({WrappedComponent:AddressList,fetchData:address.fetchCompanyMailAddresses,props:{addressType:'寄票地址',editAddress:address.editClick}})
      return component
    }
  }
</script>

<style scoped>

</style>

總結(jié):可復(fù)用性高,但可拓展性差。

Vue3高階組件demo地址

React跟Vue中高階組件使用感受:
React本身的框架就支持高階組件,因此在使用上,無論是引用還是代碼的可讀性,都特別友好。
而在Vue中,本身就很少提及,因此我們需要使用函數(shù)式編程的思想以及Vue本身擁有的API去封裝一個(gè)高階組件,是能實(shí)現(xiàn),但效果并沒有React的友好。代碼可讀性差,引用不方便等等。因此,什么時(shí)候使用高階組件,還是要看實(shí)際的業(yè)務(wù)場景以及業(yè)務(wù)需求。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容