Featured image of post 「React深入」高阶组件——HOC

「React深入」高阶组件——HOC

React的很多社区库的设计模式都是HOC(如 React-Redux、React-Router 等),理解HOC,可以更深入理解React

注: 原文地址 ,作者:小杜杜

本站拷贝一份是为方便查看,以及本站文章搜索。非商业用途,免费查看,侵删!!!

如果这篇文章对你有帮助,请多支持原作者!!!

在React面试中,我们常常会问到HOC,包括其他很多源码,实现方式都与HOC脱不了关系,看似简单的介绍,实际上也不简单,看完这篇文章后一定让你对HOC有更加深刻的理解,还请各位小伙伴多多支持~

高阶组件可能并没有你想象的那么简单,它可以做很多事情,接下来我们来看看以下几个问题:

  • 什么是高阶组件,它的作用是什么?
  • 高阶组件的编写结构是什么?
  • 高阶组件如何编写,如何发挥应有的作用?
  • 高阶组件可以做什么,如何制定一个公共化的高阶组件?

React深入:HOC.png 如果你对以上问题有疑问,那么这篇文章应该能够很好的帮助到各位。

高阶组件到底是什么?

高阶组件:也叫HOC,它是一种复用组件逻辑的一种高级技巧,并且 HOC 自身不是 React API 的一部分,而是基于React 的组合特性而形成的设计模式。

那么什么样的组件可以被称作 HOC 呢?

如果一个组件接收的参数是一个组件,并且返回也是一个组件,那么该组件就是高阶组件(HOC)

我们发现,HOC的参数和返回都是一个组件,那么我们可以理解为HOC就是对这个组件进行加工强化,从而提高组件的复用逻辑复杂程度渲染性能

画成图就是这样:

image.png 我们从图中可以看出,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属性,我们先来看看此时的效果:

image.png

此时 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就是Indexprops,我们引用下 <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用到过,用于处理reduxstate的更改,带来的订阅更新作用。
 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);

效果:

img1.gif

这种方式实际上,就是传入对应的方法,然后去执行就好了,跟子传父一样

条件渲染

条件渲染: 需要一个条件来控制是否渲染,通过条件来进行触发,而不是操作组件内部控制渲染,通常运用在路由加载页面懒加载

我们直接来看以下这个例子:

 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);

效果:

img3.gif 我们发现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;

效果展示:

img2.gif

深入:异步组件

关于异步组件的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++}>count1</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是否会重新刷新:

效果: img4.gif

照理而言,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再来看看效果:

img5.gif

这样我们就已经解决了这个问题。

深入:定制为公用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;

效果展示:

img7.gif 可以看到,通过HOC包裹后,通过不同获取元素的方法,可以将对应的事件进行劫持,但这里需要注意一点,此处的HOC并没有阻碍原来的点击事件,只是在其基础上增加功能,并且执行顺序优于原来的事件

反向继承

HOC可以通过反向继承模式,通过劫持类组件的render函数,并且可以对propschildren进行更改,同时也可以劫持类组件的生命周期,或增加生命周期

我们上面讲的增强props抽离state条件渲染等都是在原有组件上进行增强或者控制,而反向继承可以更改原组件的形式,接下来我们一起看看:

渲染劫持

HOC可以通过super.render()来获取到对应元素,再配合React.createElementReact.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);

效果展示:

image.png

可以看到,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);

效果展示: image.png

个人理解

这块知识作为了解就好,在实际开发中尽量不要使用,主要有以下两点原因(有说的不对的地方欢迎评论区指出):

  • 第一点:由于Hooks的盛行,已经很少使用class组件,而反向继承只能用于class组件,所以这一点成为了根本的限制
  • 第二点:隐患比较大,因为我们可以劫持到对应的生命周期,那么就会具有多个生命周期,当多个生命周期串联在一起,有可能造成很大的副作用,这一点并不适合在复杂的页面中使用

End

参考

这篇文章参考 我不是外星人 这位大佬的,看过很多HOC的文章,都没这位大佬的细,看完后真的受益良多

本篇文章,按照自己的理解所整理的,只希望可以增加自己的知识储备,提升自己,还请各位大佬勿喷~

总结

  • 高阶组件(HOC)其概念非常简单,就是接收一个组件再返回一个组件
  • 由于hooks的流行并不推荐使用反像继承,因为此功能无法运用在函数组件上,当然最根本的原因是使用这种方式的隐患比较大
  • 高阶组件应该是无副作用的纯函数,所以不要在render中使用,且谨慎修改原型链
  • 高阶组件的初衷是做到公共化,虽然在实际项目中可能用到的不是很多,但HOC确实是React不可缺少的一部分,好多功能都用到了HOC,像路由的懒加载,缓存页面等

最后

相信在这篇文章的帮助下,各位小伙伴应该跟我一样对HOC有了更深的理解,当然,实践是检验真理的唯一标准,多多敲代码才是王道~

另外,觉得这篇文章能够帮助到你的话,请点赞+收藏一下吧,顺便关注下专栏,之后会输出有关React的好文,一起上车学习吧~

前往原文支持

其他React好文