注:
原文地址
,作者:小杜杜
本站拷贝一份是为方便查看,以及本站文章搜索。非商业用途,免费查看,侵删!!!
如果这篇文章对你有帮助,请多支持原作者!!!
在React面试中,我们常常会问到HOC
,包括其他很多源码,实现方式都与HOC脱不了关系,看似简单的介绍,实际上也不简单,看完这篇文章后一定让你对HOC
有更加深刻的理解,还请各位小伙伴多多支持~
高阶组件可能并没有你想象的那么简单,它可以做很多事情,接下来我们来看看以下几个问题:
- 什么是高阶组件,它的作用是什么?
- 高阶组件的编写结构是什么?
- 高阶组件如何编写,如何发挥应有的作用?
- 高阶组件可以做什么,如何制定一个公共化的高阶组件?
- …
如果你对以上问题有疑问,那么这篇文章应该能够很好的帮助到各位。
高阶组件到底是什么?
高阶组件:也叫HOC,它是一种复用组件逻辑的一种高级技巧,并且 HOC 自身不是 React API 的一部分,而是基于React 的组合特性而形成的设计模式。
那么什么样的组件可以被称作 HOC
呢?
如果一个组件接收的参数是一个组件,并且返回也是一个组件,那么该组件就是高阶组件(HOC)
我们发现,HOC
的参数和返回都是一个组件,那么我们可以理解为HOC
就是对这个组件进行加工
、强化
,从而提高组件的复用逻辑
、复杂程度
、渲染性能
画成图就是这样:
我们从图中可以看出,hoc 是完全包于组件A的,可以说是组件A的超集, 所以我们应该注意经过包装的组件A强化了那些,增加了那些功能,解决了那些缺陷,这些才是 HOC
的意义所在
为了更好的理解上面这个图,做个小栗子🌰:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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
来看看
1
2
3
4
5
|
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
,所以这种写法并不推荐,但你要了解,这一点要牢记~
装饰器写法
使用 @, 如:
1
2
3
4
5
6
|
@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
的更改,带来的订阅更新作用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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);
|
效果:
这种方式实际上,就是传入对应的方法,然后去执行就好了,跟子传父
一样
条件渲染
条件渲染: 需要一个条件来控制是否渲染,通过条件来进行触发,而不是操作组件内部控制渲染,通常运用在路由加载页面
、懒加载
上
我们直接来看以下这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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;
|
代码展示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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'))
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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来写,帮助大家更好的熟悉~
小栗子🌰:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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
来对我们的组件进行优化:
1
2
3
4
5
6
7
8
|
import useCreation from '../useCreation';
const HOC = (Component:any) => (props:any) => {
return useCreation(() => <Component {...props} />, [props.count])
}
export default HOC;
|
我们包裹完Child
再来看看效果:
这样我们就已经解决了这个问题。
深入:定制为公用HOC
聪明的小伙伴发现了一个问题,那就是上述的HOC
只能运用在当前的组件下,因为子组件的变量并不是一个特定的值,并没有做到公共化,这样就违背了HOC
的初衷,所以我们需要为刚才的HOC
进行升级,也就是需要一个特定的条件来控制是否渲染
1
2
3
4
5
6
7
|
import useCreation from '../useCreation';
const HOC = (rule: (props:any) => void) => (Component:any) => (props:any) => {
return useCreation(() => <Component {...props} />, [rule(props)])
}
export default HOC;
|
我们再传递一个函数,来作为useCreation
的依赖项
使用:
1
|
const ChildHoc = HOC((props:any)=> props['count'])(Child)
|
这样就能达到通用的效果。
与此同时,我们可以利用这个高阶组件来做性能优化,提高页面的性能,会非常方便的
事件赋能
HOC
还可以做到事件赋能
的功能,这种场景可以运用处理额外功能上面(如:埋点),我们可以监听到对应的事件上,处理一些额外的事情。
首先,我们需要将HOC
做成定制化的,其次是需要监听对象的目标、方式和处理函数本身的方法
在这里我选用的是querySelector
来监听对应的目标,用addEventListener来监听事件
HOC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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就做好了,接下来看看这个小栗子🌰:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
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的妙用。
栗子🌰:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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) 获取对应的生命周期函数,从而达到劫持生命周期的效果
栗子🌰:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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好文
: