由高階函數(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é)果展示:

以上是一個(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ù)用。

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

重寫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

使用場景:跟蹤組件性能
/**
* 反向繼承 跟蹤組件性能
*/
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)

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>

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