大家好,我是小杜杜,今天我们来说说高阶组件,相信各位小伙伴对它并不陌生,多多少少都知道点,然而高阶组件对我而言既熟悉又陌生,熟悉是我确实知道它,陌生是我不知道如何去使用它,不知道小伙伴有没有这样一种感觉~
在React面试中,我们常常会问到HOC
,包括其他很多源码,实现方式都与HOC脱不了关系,看似简单的介绍,实际上也不简单,看完这篇文章后一定让你对HOC
有更加深刻的理解,还请各位小伙伴多多支持~
高阶组件可能并没有你想象的那么简单,它可以做很多事情,接下来我们来看看以下几个问题:
- 什么是高阶组件,它的作用是什么?
- 高阶组件的编写结构是什么?
- 高阶组件如何编写,如何发挥应有的作用?
- 高阶组件可以做什么,如何制定一个公共化的高阶组件?
- ...
如果你对以上问题有疑问,那么这篇文章应该能够很好的帮助到各位。
高阶组件到底是什么?
高阶组件:也叫HOC,它是一种复用组件逻辑的一种高级技巧,并且 HOC 自身不是 React API 的一部分,而是基于React 的组合特性而形成的设计模式。
那么什么样的组件可以被称作 HOC
呢?
如果一个组件接收的参数是一个组件,并且返回也是一个组件,那么该组件就是高阶组件(HOC)
我们发现,HOC
的参数和返回都是一个组件,那么我们可以理解为HOC
就是对这个组件进行加工
、强化
,从而提高组件的复用逻辑
、复杂程度
、渲染性能
画成图就是这样:
我们从图中可以看出,hoc 是完全包于组件A的,可以说是组件A的超集, 所以我们应该注意经过包装的组件A强化了那些,增加了那些功能,解决了那些缺陷,这些才是
HOC
的意义所在
为了更好的理解上面这个图,做个小栗子🌰:
import { Button } from 'antd-mobile';
import React, {useState} from 'react';
const HOC = (Component:any) => (props:any) => {
return <Component name={"小杜杜"} {...props}></Component>
}
const Index:React.FC<any> = (props)=> {
const [flag, setFlag] = useState<boolean>(false)
return (
<div>
<Button color="primary" onClick={() => setFlag(true)} > 获取props </Button>
{flag && <div>{JSON.stringify(props)}</div>}
</div>
);
}
export default HOC(Index);
我们可以看到 HOC
就是一个高阶组件,他给 Index
增加了个name
属性,我们先来看看此时的效果:
此时 Index 已经获得了 name 这个属性。
有的小伙伴可能不熟悉
HOC
的写法,接下来简单说下:
我们把 HOCComponent
翻译成 ES5
来看看
var HOC = function (Component) {
return function (props) {
return React.createElement(Component, __assign({ name: "小杜杜" }, props));
};
};
实际上第二个props
就是Index
的props
,我们引用下 <Index age={7} />
,此时的HOCComponent
就是{age: 7}
高阶组件的编写结构
HOC 在使用上有两种模式:装饰器写法
和 函数包裹模式
其中 函数包裹模式
就是上述的例子,接下来说说 装饰器写法
。
装饰器写法
只能用在 class
组件中,并且需要做额外的配置,由于现在函数式
已经逐渐取代了class
,所以这种写法并不推荐,但你要了解,这一点要牢记~
装饰器写法
使用 @, 如:
@HOC3
@HOC2
@HOC1
class Index extends React.Component{
}
这里要注意一下包装的顺序,越靠近Index
组件就是越内层的HOC
翻译成函数式就是这样:HOC3( HOC2( HOC1( Index ) ) )
高阶组件可以做那些事
在这里我把高阶组件的作用分为强化Props
、条件渲染
、性能优化
、事件赋能
、反向继承
五大类,其中强化Props
是最常用的一种方式,性能优化
可以结合hooks
进行优化,反向继承
以不推荐使用,接下来我们逐一看看
强化 Props
强化Props
主要有两种方式,分别是混入props
和抽离state控制更新
- 混入props:这个是HOC最为常见的功能,通过承接上层的props,混入到自己的props,以此达到强化组件的作用。
上述的栗子🌰就是混入props,在原有的Index
中混入了name
属性
抽离state控制更新
- 抽离state控制更新:HOC 可以将自身的
state
配合起来使用,用于对组件的更新。这种使用方式在react-redux
中的connect
用到过,用于处理redux
中state
的更改,带来的订阅更新作用。
import { Button } from 'antd-mobile';
import React, {useState} from 'react';
const HOC = (Component:any) => (props:any) => {
const [number, setNumber] = useState<number>()
return <Component
number={`你已为小杜杜点赞${number}次数`}
onChange={(value:number) => {setNumber(value)}}
{...props}
/>
}
const Index:React.FC<any> = (props)=> {
const [count, setCount] = useState<number>(0)
const { number, onChange } = props
return (
<div>
<Button color="primary" onClick={() => setCount(res => res + 1)} > 累积点赞 </Button>
<div>{count}</div>
<Button color="primary" onClick={() => onChange(count)} > 同步 </Button>
<div>{number}</div>
</div>
);
}
export default HOC(Index);
效果:
这种方式实际上,就是传入对应的方法,然后去执行就好了,跟子传父
一样
条件渲染
条件渲染: 需要一个条件来控制是否渲染,通过条件来进行触发,而不是操作组件内部控制渲染,通常运用在路由加载页面
、懒加载
上
我们直接来看以下这个例子:
import { Button, DotLoading } from 'antd-mobile';
import React, {useState} from 'react';
import img from './img.jpeg'
const HOC = (Component:any) => (props:any) => {
const [show, setShow] = useState<boolean>(false)
return <div>
<Button color="primary" onClick={() => setShow(v => !v)}>加载图片</Button>
{show ? <Component {...props}/> : <div style={{margin: 25}}><DotLoading color='primary' />加载中</div>}
</div>
}
const Index:React.FC<any> = ({})=> {
return (
<div>
<img src={img} width={160} height={120} alt="" />
</div>
);
}
export default HOC(Index);
效果:
我们发现
HOC
的作用是控制Index
组件是否加载的,如果在没加载出来则给个加载的小样式。
深入:分片渲染
当数据量非常大的时候,我们执行子组件很多的情况下(如列表),如果一次性加载,可能会出现卡顿
、长时间白屏
的情况,这时使用分片渲染是一个不错的优化的方案。
分片渲染:简单的说就是一个执行完再执行下一个,其思想是建立一个队列,通过定时器来进行渲染,比如说一共有3次,先把这三个放入到数组中,当第一个执行完成后,并剔除执行完成的,在执行第二个,直到全部执行完毕,渲染队列清空。
HOC:
import { useEffect, useState } from 'react';
import { DotLoading } from 'antd-mobile';
const waitList:any = [] //等待队列
let isRender:boolean = false //控制渲染条件
const waitRender = () => {
const res = waitList.shift()
if(!res) return
setTimeout(() => {
res()
}, 300)
}
const HOC = (Component:any) => (props:any) => {
const [show, setShow] = useState<boolean>(false)
useEffect(() => {
waitList.push(() => {setShow(true)})
if(!isRender){
waitRender()
isRender = true
}
}, [])
return show ? <Component waitRender={waitRender} {...props}/> : <div style={{margin: 25}}><DotLoading color='primary' />加载中</div>
}
export default HOC;
代码展示:
import React, {useEffect} from 'react';
import img from './img.jpeg'
import { HOC } from '@/components'
// 子组件
const Child:React.FC<{name: string, waitRender: () => void}> = ({name, waitRender}) => {
useEffect(() => {
waitRender()
}, [])
return (
<div>
<img src={img} width={160} height={120} alt="" />{name}
</div>
)
}
const Item = HOC(Child)
const Index:React.FC<any> = ()=> {
const list = [{ name: '图片1'}, { name: '图片2' }, { name: '图片3' }]
return (
<div>
{
list.map((item) => <Item name={item.name} key={item.name} />)
}
</div>
);
}
export default Index;
效果展示:
深入:异步组件
关于异步组件的HOC这块常常运用在路由,用来加载对应的页面,如:dva 的 dynamic
,关于这块的内容,有兴趣的小伙伴可以详细看看:Loading components asynchronously in React app with an HOC 这篇文章
我们直接来看下异步组件HOC的代码:
import { useEffect, useState } from 'react';
const HOC = (Component:any) => (props:any) => {
const [com, setCom] = useState<any>({})
useEffect(() => {
Component().then((cmp:any) => {
setCom({ default: cmp.default})
})
}, [])
if(com.default){
const C = com.default
return <C {...props} />
}
return null
}
export default HOC;
当每次调用的时候,React都会尝试引入这个组件,它将会自动加载一个包含该组件的chunk.js。
使用方式:
const AsyncButton = HOC(() => import('../../components/Button'))
例子:
import React from 'react';
import { HOC } from '@/components'
const AsyncButton = HOC(() => import('../../components/Button'))
const Index:React.FC<any> = () => {
return (
<div>
<AsyncButton>异步按钮</AsyncButton>
</div>
);
}
export default Index;
这里定义的AsyncButton
并不是在DOM
中直接定义Button
组件,当AsyncButton
被挂载到DOM
时,会调用Component
函数,然后返回一个Button
组件,所以在import
这个操作完成前,这个地方的DOM
都是空的,当执行操作后,又会添加到AsyncButton
中,从而重新触发渲染和加载
所以异步组件的HOC
常常与路由进行配合使用,对于我们来说,加载的页面其本质也是组件(容器组件),也就是说,我们只需要一开始访问的页面就行加载就可以了,剩下的页面按需加载
即可,使用上和上述的Button
一样
性能优化
在上一篇文章中,本菜鸟详细讲解了有关自定义Hooks的实战,相信阅读过的小伙伴已经可以优雅的实现各种hooks
,相同的,高阶组件也可以配合对应的hooks
做性能优化。
感兴趣的小伙伴可以看看:搞懂这12个Hooks,保证让你玩转React
这个小栗子🌰也会运用上一篇文章的自定义hooks来写,帮助大家更好的熟悉~
小栗子🌰:
import { Button } from 'antd-mobile';
import React from 'react';
import { useReactive } from '@/components'
// 子组件
const Child:React.FC<any> = (props) => {
return <div style={{marginBottom: 8}}>
{console.log('渲染')}
数字: {props.count}
</div>
}
const Index:React.FC<any> = (props)=> {
const state = useReactive<any>({
count: 0,
flag: false
})
return (
<div style={{padding: 20}}>
<Child count={state.count} />
<Button color='primary' onClick={() => state.count++}>count加1</Button>
<Button style={{marginLeft: 8}} color='primary' onClick={() => state.flag = !state.flag} >切换状态:{JSON.stringify(state.flag)}</Button>
</div>
);
}
export default Index;
我们可以看到,子组件Child
的值与count
有关,与flag
无关,但我们来切换flag
的状态,看看Child
是否会重新刷新:
效果:
照理而言,flag
是与 Child
毫无关系的,但改变时,还是会触发,很明显我们并不希望造成无关的渲染,所以HOC
也可以通过结合hooks
来对我们的组件进行优化:
import useCreation from '../useCreation';
const HOC = (Component:any) => (props:any) => {
return useCreation(() => <Component {...props} />, [props.count])
}
export default HOC;
我们包裹完Child
再来看看效果:
这样我们就已经解决了这个问题。
深入:定制为公用HOC
聪明的小伙伴发现了一个问题,那就是上述的HOC
只能运用在当前的组件下,因为子组件的变量并不是一个特定的值,并没有做到公共化,这样就违背了HOC
的初衷,所以我们需要为刚才的HOC
进行升级,也就是需要一个特定的条件来控制是否渲染
import useCreation from '../useCreation';
const HOC = (rule: (props:any) => void) => (Component:any) => (props:any) => {
return useCreation(() => <Component {...props} />, [rule(props)])
}
export default HOC;
我们再传递一个函数,来作为useCreation
的依赖项
使用:
const ChildHoc = HOC((props:any)=> props['count'])(Child)
这样就能达到通用的效果。
与此同时,我们可以利用这个高阶组件来做性能优化,提高页面的性能,会非常方便的
事件赋能
HOC
还可以做到事件赋能
的功能,这种场景可以运用处理额外功能上面(如:埋点),我们可以监听到对应的事件上,处理一些额外的事情。
首先,我们需要将HOC
做成定制化的,其次是需要监听对象的目标、方式和处理函数本身的方法
在这里我选用的是querySelector
来监听对应的目标,用addEventListener来监听事件
HOC:
import { useEffect } from 'react';
interface Props{
target: string,
way?: string,
handler: () => void
}
const HOC = ({target, way = 'click', handler=()=>{}}:Props) => (Component:any) => (props:any) => {
useEffect(() => {
const res = document.querySelector(target);
res?.addEventListener(way, handler)
return () => {
res?.removeEventListener(way, handler)
}
}, [])
return <Component {...props} />
}
export default HOC;
这样一个简易版的监听事件的HOC就做好了,接下来看看这个小栗子🌰:
import { Button, Toast } from 'antd-mobile';
import React from 'react';
import { HOC } from '@/components'
// 子组件
const Child = () => {
return <div>
<Button >赋能按钮</Button>
</div>
}
const Child1 = () => {
return <div id="id" style={{marginTop: 10, background: 'gold', cursor: 'pointer'}}>
赋能id
</div>
}
const Child2 = () => {
return <div className='class' style={{marginTop: 10, background: 'violet', cursor: 'pointer'}}>
赋能class
</div>
}
const CHildHoc = HOC({
target: 'button',
handler: () => {
Toast.show('按钮赋能')
}
})(Child)
const CHildHoc1 = HOC({
target: '#id',
handler: () => {
Toast.show('id赋能')
}
})(Child1)
const CHildHoc2 = HOC({
target: '.class',
handler: () => {
Toast.show('class赋能')
}
})(Child2)
const Index:React.FC<any> = (props)=> {
return (
<div style={{padding: 20}}>
<CHildHoc />
<CHildHoc1 />
<CHildHoc2 />
</div>
);
}
export default Index;
效果展示:
可以看到,通过HOC包裹后,通过不同获取元素的方法,可以将对应的事件进行劫持,但这里需要注意一点,此处的HOC并没有阻碍原来的点击事件,只是在其基础上增加功能,并且执行顺序优于原来的事件
反向继承
HOC
可以通过反向继承模式,通过劫持类组件的render函数,并且可以对props
、children
进行更改,同时也可以劫持类组件的生命周期,或增加生命周期
我们上面讲的增强props
、抽离state
、条件渲染
等都是在原有组件上进行增强或者控制,而反向继承可以更改原组件的形式,接下来我们一起看看:
渲染劫持
HOC可以通过super.render()
来获取到对应元素,再配合React.createElement
、React.cloneElement
、 React.Children
等Api对元素进行操作,实现更换节点,修改props的妙用。
栗子🌰:
import React from 'react';
function HOC (Component:any){
return class Advance extends Component {
render() {
const element = super.render()
const appendElement = React.createElement('div' ,{} , `大家好,我是小杜杜` )
const res = React.Children.map(element.props.children,(child,index)=>{
if(index === 1) return appendElement
return child
})
return React.cloneElement(element, element.props, res)
}
}
}
class Index extends React.Component{
render(){
return <div>
<p>劫持元素</p>
<p>大家好,我是React</p>
</div>
}
}
export default HOC(Index);
效果展示:
可以看到,Index
原本渲染的是我是React
,但我们劫持后更改变成了小杜杜
,那是不是可以用这种方法来更换署名呢?🤔当我没说~
劫持生命周期
我们可以通过原型(prototype) 获取对应的生命周期函数,从而达到劫持生命周期的效果
栗子🌰:
import React from 'react';
function HOC (Component:any){
const didMount = Component.prototype.componentDidMount;
Component.prototype.componentDidMount = function(){
console.log('劫持生命周期:componentDidMount')
didMount.call(this)
}
return class Index extends React.Component{
render(){
return <Component {...this.props} />
}
}
}
class Index extends React.Component{
componentDidMount(){
console.log('---componentDidMount---')
}
render(){
return <div>大家好,我是小杜杜</div>
}
}
export default HOC(Index);
效果展示:
个人理解
这块知识作为了解就好,在实际开发中尽量不要使用,主要有以下两点原因(有说的不对的地方欢迎评论区指出):
- 第一点:由于Hooks的盛行,已经很少使用class组件,而反向继承只能用于class组件,所以这一点成为了根本的限制
- 第二点:隐患比较大,因为我们可以劫持到对应的生命周期,那么就会具有多个生命周期,当多个生命周期串联在一起,有可能造成很大的副作用,这一点并不适合在复杂的页面中使用
End
参考
这篇文章参考 我不是外星人 这位大佬的,看过很多HOC
的文章,都没这位大佬的细,看完后真的受益良多
本篇文章,按照自己的理解所整理的,只希望可以增加自己的知识储备,提升自己,还请各位大佬勿喷~
总结
- 高阶组件(HOC)其概念非常简单,就是接收一个
组件
再返回一个组件
- 由于hooks的流行并不推荐使用反像继承,因为此功能无法运用在函数组件上,当然最根本的原因是使用这种方式的隐患比较大
- 高阶组件应该是无副作用的纯函数,所以不要在
render
中使用,且谨慎修改原型链 - 高阶组件的初衷是做到公共化,虽然在实际项目中可能用到的不是很多,但
HOC
确实是React
不可缺少的一部分,好多功能都用到了HOC
,像路由的懒加载,缓存页面等
最后
相信在这篇文章的帮助下,各位小伙伴应该跟我一样对HOC
有了更深的理解,当然,实践是检验真理的唯一标准,多多敲代码才是王道~
另外,觉得这篇文章能够帮助到你的话,请点赞+收藏一下吧,顺便关注下专栏,之后会输出有关React
的好文,一起上车学习吧~
其他React好文
:
- 搞懂这12个Hooks,保证让你玩转React
- 「React深入」一文吃透虚拟DOM和diff算法
- 花三个小时,完全掌握分片渲染和虚拟列表~
- 「React 深入」一文吃透React v18全部Api(1.3w+)
玩转 React Hooks 小册
小册链接:《玩转 React Hooks》
知其然,知其所以然。React Hooks 带来的全新机制让人耳目一新,因为它拓展了 React 的开发思路,为 React 开发者提供了一种更方便、更简洁的选择。
在引入 Hooks 的概念后,函数组件既保留了原本的简洁,也具备了状态管理、生命周期管理等能力,在原来 Class 组件所具备的能力基础上,还解决了 Class 组件存在的一些代码冗余、逻辑难以复用等问题。因此,在如今的 React 中,Hooks 已经逐渐取代了 Class 的地位,成了主导。
而且,Hooks 相对于 Class 而言,更容易上手,其简洁性、逻辑复用性
等特性深受开发者喜爱,可谓是前端界的"流量明星"
,不止 React,Vue 3.0 、Preact、Solid.js 等框架也都选择加入 Hooks 的大家庭,前端的日常工作也在趋向于 Hooks 开发。
因此,掌握好 React Hooks 是非常有必要的一件事。本小册会通过基础篇、原码篇、实践篇 三大方向
探讨 Hooks,从原码的角度探寻 React 的奥秘。
除此之外,小册会以 React Hooks 为核心,同时穿插其他知识,如 TS、Jest、Fiber 等核心知识,并包含 React v18 的并发、数据撕裂等概念,最后结合 Hooks 写一个简易版 react-redux 和 Form 表单,通过其设计思想,助你在面试中脱颖而出。
小册整体设计如下思维导图
所示:
你会学到什么?
- 全面知悉 React 提供的 15 Hooks API 的使用和场景;
- 手写 30+ 自定义 Hooks 的实现,全面掌握设计思想;
- 了解 Hooks 源码,从根源上彻底解决现有的难点;
- 掌握函数式编程思想,用于工作,享受便利。
最后
感谢各位小伙伴的支持,如果在阅读过程中有什么问题欢迎大家加我微信,交个朋友,微信:domesyPro, 也可以关注笔者的公众号:杜杜的全栈之旅,一起来玩转 React Hooks 吧~