react与vue中高阶组件的对比

由高阶函数引申出来的高阶组件

高阶组件本质上也是一个函数,并不是一个组件,且高阶组件是一个纯函数。
高阶组件,顾名思义,就是传入一个组件,输出一个组件。

1 React中的高阶组件

一个最简单的例子:

// 高阶组件
import React,{Component} from 'react';

export default function withHeader(WrappedComponent) {
  return class HOC extends Component{
    render() {
      return <div>
        <div className="demo-header">
          我是高阶组件的标题
        </div>
        <WrappedComponent {...this.props} title={'我是高阶组件过来的title'} />
      </div>
    }
  }
}

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

结果展示:


image.png

以上是一个简单的例子,但并没办法体现出高阶组件的好处。

高阶组件的实现方式有两种:属性代理以及反向继承

1.1 属性代理

把变的部分(组件和获取数据的方法) 抽离到外部作为传入,从而实现页面的复用

应用场景:不同类型的地址列表展示

传统方式:先定义一个可复用的组件,再根据不同类型查询数据再引用

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>暂无数据</div>
    )
  }

}
export default TraditionWay

在这里,多个类型的话,就需要写多个类似的页面。数据查询与组件引用均需写多次,显得代码有些重复。
如果我们使用高阶组件,则可以这样实现

// 高阶组件
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});

显然,代码没有那么冗余,也方便了复用。


image.png
1.2 反向继承

高阶组件可以通过this直接访问原组件的state/ref/生命周期方法,
作用:劫持渲染、操作state

主要是调用

super.render()

一个简单的例子是:

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

class ButtonItem extends Component{
  constructor(props) {
    super(props);
    this.state = {
      title: 'buttonTitle'
    }
  }
  clickComponent(){
    console.log('按钮点击')
  }
  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 点击</div>
        <div>{super.render()}</div>
      </div>
    )
  }
}

export default ButtonHOC
image.png

重写state跟clickComponent的情况下,原组件的state与方法都会被覆盖

// 反向继承高阶组件
import React from 'react';

const ButtonHOC = (WrappedComponent)=>  class extends WrappedComponent {
  constructor(props) {
    super(props);
    this.state = {
      title: 'HOC继承'
    }
  }
  clickComponent(){
    console.log('HOC继承点击')
  }
  render(){
    return (
      <div>
        <div onClick={this.clickComponent}>ButtonHOC 点击</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} 组件渲染时间为 ${this.end - this.start} ms`);
    }

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

export default withTiming(Children)

image.png

React高阶组件demo代码地址

2 Vue2.0 中的高阶组件

由于Vue官方不怎么推崇HOC,而且Mixins本身就能实现HOC的相关功能,所以Vue中对HOC的支持并不是很好。但我们还是可以强行在Vue中使用HOC,看看他到底怎样。

// 封装高阶函数
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

// 高阶函数的使用
<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 中的高阶组件

这里将通过两种方式实现所谓的高阶组件,用到的知识点主要是Vue3中的具名插槽以及组合式 Api,公共自组件我们暂时不说,这里直接进入主题,看看如何写出一个可复用的高阶组件

1.是使用具名插槽以及组合式API实现的组件

// fetch.vue
// 定义具名插槽
<template>
  <div>
  // 数据
    <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>
// 引用封装好的函数
  import { useFetch, usePagination } from "../composables/HocFetch";
  import { toRefs } from "vue";

  export default {
    name: "Fetch",
    props: {
      paginate: Boolean,
      endpoint: String
    },
    setup(props) {
    // 使用toRefs的作用是:直接解构props会使props失去响应性,所以这里需要使用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
// 这里将分页跟查询分离,通过监听页数变化查询数据
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>当前是第{{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>

总结:可复用且可拓展性高,但使用不方便,代码不够简洁。

使用组合式Api以及函数式组件实现高阶组件,利用setup中return component的功能实现

// 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',`当前是第${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>

总结:可复用性高,但可拓展性差。

Vue3高阶组件demo地址

React跟Vue中高阶组件使用感受:
React本身的框架就支持高阶组件,因此在使用上,无论是引用还是代码的可读性,都特别友好。
而在Vue中,本身就很少提及,因此我们需要使用函数式编程的思想以及Vue本身拥有的API去封装一个高阶组件,是能实现,但效果并没有React的友好。代码可读性差,引用不方便等等。因此,什么时候使用高阶组件,还是要看实际的业务场景以及业务需求。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容