由高阶函数引申出来的高阶组件
高阶组件本质上也是一个函数,并不是一个组件,且高阶组件是一个纯函数。
高阶组件,顾名思义,就是传入一个组件,输出一个组件。
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)
结果展示:
以上是一个简单的例子,但并没办法体现出高阶组件的好处。
高阶组件的实现方式有两种:属性代理以及反向继承
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});
显然,代码没有那么冗余,也方便了复用。
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
重写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
使用场景:跟踪组件性能
/**
* 反向继承 跟踪组件性能
*/
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)
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>
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>
总结:可复用性高,但可拓展性差。
React跟Vue中高阶组件使用感受:
React本身的框架就支持高阶组件,因此在使用上,无论是引用还是代码的可读性,都特别友好。
而在Vue中,本身就很少提及,因此我们需要使用函数式编程的思想以及Vue本身拥有的API去封装一个高阶组件,是能实现,但效果并没有React的友好。代码可读性差,引用不方便等等。因此,什么时候使用高阶组件,还是要看实际的业务场景以及业务需求。