「react进阶」一文吃透React高阶组件(HOC) (上)

简介: 从一篇文章彻底介绍 React 高阶组件

一 前言

React高阶组件(HOC),对于很多react开发者来说并不陌生,它是灵活使用react组件的一种技巧,高阶组件本身不是组件,它是一个参数为组件,返回值也是一个组件的函数。高阶作用用于强化组件,复用逻辑,提升渲染性能等作用。高阶组件也并不是很难理解,其实接触过后还是蛮简单的,接下来我将按照,高阶组件理解?高阶组件具体怎么使用?应用场景高阶组件实践(源码级别) 为突破口,带大家详细了解一下高阶组件。本文篇幅比较长,建议收藏观看

我们带着问题去开始今天的讨论:

  • 1 什么是高阶组件,它解决了什么问题?
  • 2 有几种高阶组件,它们优缺点是什么?
  • 3 如何写一个优秀高阶组件?
  • 4 hoc怎么处理静态属性,跨层级ref等问题?
  • 5 高阶组件怎么控制渲染,隔离渲染?
  • 6 高阶组件怎么监控原始组件的状态?
  • ...

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

NAOTU.jpg

二 全方位看高阶组件

1 几种包装强化组件的方式

① mixin模式

原型图

C32587B9-D0FB-46CA-9AF8-FE2DF49021E5.jpg

老版本的react-mixins

react初期提供一种组合方法。通过React.createClass,加入mixins属性,具体用法和vue
mixins相似。具体实现如下。

const customMixin = {
   
   
  componentDidMount(){
   
   
    console.log( '------componentDidMount------' )
  },
  say(){
   
   
    console.log(this.state.name)
  }
}

const APP = React.createClass({
   
   
  mixins: [ customMixin ],
  getInitialState(){
   
   
    return {
   
   
      name:'alien'
    }
  },
  render(){
   
   
    const {
   
    name  } = this.state
    return <div> hello ,world , my name is {
   
    name } </div>
  }
})

这种mixins只能存在createClass中,后来React.createClass连同mixins这种模式被废弃了。mixins会带来一些负面的影响。

  • 1 mixin引入了隐式依赖关系。
  • 2 不同mixins之间可能会有先后顺序甚至代码冲突覆盖的问题
  • 3 mixin代码会导致滚雪球式的复杂性

衍生方式

createClass的废弃,不代表mixin模式退出react舞台,在有状态组件class,我们可以通过原型链继承来实现mixins

const customMixin = {
   
     /* 自定义 mixins */
  componentDidMount(){
   
   
    console.log( '------componentDidMount------' )
  },
  say(){
   
   
    console.log(this.state.name)
  }
}

function componentClassMixins(Component,mixin){
   
    /* 继承 */
  for(let key in mixin){
   
   
    Component.prototype[key] = mixin[key]
  }
}

class Index extends React.Component{
   
   
  constructor(){
   
   
    super()
    this.state={
   
     name:'alien' }
  }
  render(){
   
   
    return <div> hello,world
      <button onClick={
   
    this.say.bind(this) } > to say </button>
    </div>
  }
}
componentClassMixins(Index,customMixin)

②extends继承模式

原型图

9F743F44-D7FD-4F81-805B-80E8D5A358DB.jpg

class组件盛行之后,我们可以通过继承的方式进一步的强化我们的组件。这种模式的好处在于,可以封装基础功能组件,然后根据需要去extends我们的基础组件,按需强化组件,但是值得注意的是,必须要对基础组件有足够的掌握,否则会造成一些列意想不到的情况发生。

class Base extends React.Component{
   
   
  constructor(){
   
   
    super()
    this.state={
   
   
      name:'alien'
    }
  }
  say(){
   
   
    console.log('base components')
  }
  render(){
   
   
    return <div> hello,world <button onClick={
   
    this.say.bind(this) } >点击</button>  </div>
  }
}
class Index extends Base{
   
   
  componentDidMount(){
   
   
    console.log( this.state.name )
  }
  say(){
   
    /* 会覆盖基类中的 say  */
    console.log('extends components')
  }
}
export default Index

③HOC模式

原型图

4F67D3DC-3B06-4B05-A006-B653D736855B.jpg

HOC是我们本章主要的讲的内容,具体用法,我们接下来会慢慢道来,我们先简单尝试一个HOC

function HOC(Component) {
   
   
  return class wrapComponent extends React.Component{
   
   
     constructor(){
   
   
       super()
       this.state={
   
   
         name:'alien'
       }
     }
     render=()=><Component {
   
    ...this.props } {
   
    ...this.state } />
  }
}

@HOC
class Index extends React.Component{
   
   
  say(){
   
   
    const {
   
    name } = this.props
    console.log(name)
  }
  render(){
   
   
    return <div> hello,world <button onClick={
   
    this.say.bind(this) } >点击</button>  </div>
  }
}

④自定义hooks模式

原型图

1956EF23-7BFA-4003-9902-4D444B329290.jpg

hooks的诞生,一大部分原因是解决无状态组件没有state逻辑难以复用问题。hooks可以将一段逻辑封装起来,做到开箱即用,我这里就不多讲了,接下来会出react-hooks原理的文章,完成react-hooks三部曲。感兴趣的同学可以看笔者的另外二篇文章,里面详细介绍了react-hooks复用代码逻辑的原则和方案。

传送门:

玩转react-hooks,自定义hooks设计模式及其实战

react-hooks如何使用?

2 高阶组件产生初衷

组件是把prop渲染成UI,而高阶组件是将组件转换成另外一个组件,我们更应该注意的是,经过包装后的组件,获得了那些强化,节省多少逻辑,或是解决了原有组件的那些缺陷,这就是高阶组件的意义。我们先来思考一下高阶组件究竟解决了什么问题🤔🤔🤔?

① 复用逻辑:高阶组件更像是一个加工react组件的工厂,批量对原有组件进行加工包装处理。我们可以根据业务需求定制化专属的HOC,这样可以解决复用逻辑。

② 强化props:这个是HOC最常用的用法之一,高阶组件返回的组件,可以劫持上一层传过来的props,然后混入新的props,来增强组件的功能。代表作react-router中的withRouter

③ 赋能组件HOC有一项独特的特性,就是可以给被HOC包裹的业务组件,提供一些拓展功能,比如说额外的生命周期,额外的事件,但是这种HOC,可能需要和业务组件紧密结合。典型案例react-keepalive-router中的 keepaliveLifeCycle就是通过HOC方式,给业务组件增加了额外的生命周期。

④ 控制渲染:劫持渲染是hoc一个特性,在wrapComponent包装组件中,可以对原来的组件,进行条件渲染节流渲染懒加载等功能,后面会详细讲解,典型代表做react-reduxconnectdvadynamic 组件懒加载。

我会针对高阶组件的初衷展开,详细介绍其原理已经用法。跟上我的思路,我们先来看一下,高阶组件如何在我们的业务组件中使用的

3 高阶组件使用和编写结构

HOC使用指南是非常简单的,只需要将我们的组件进行包裹就可以了。

使用:装饰器模式和函数包裹模式

对于class声明的有状态组件,我们可以用装饰器模式,对类组件进行包装:

@withStyles(styles)
@withRouter
@keepaliveLifeCycle
class Index extends React.Componen{
   
   
    /* ... */
}

我们要注意一下包装顺序,越靠近Index组件的,就是越内层的HOC,离组件Index也就越近。

对于无状态组件(函数声明)我们可以这么写:

function Index(){
   
   
    /* .... */
}
export default withStyles(styles)(withRouter( keepaliveLifeCycle(Index) ))

模型:嵌套HOC

对于不需要传递参数的HOC,我们编写模型我们只需要嵌套一层就可以,比如withRouter,

function withRouter(){
   
   
    return class wrapComponent extends React.Component{
   
   
        /* 编写逻辑 */
    }
}

对于需要参数的HOC,我们需要一层代理,如下:

function connect (mapStateToProps){
   
   
    /* 接受第一个参数 */
    return function connectAdvance(wrapCompoent){
   
   
        /* 接受组件 */
        return class WrapComponent extends React.Component{
   
     }
    }
}

我们看出两种hoc模型很简单,对于代理函数,可能有一层,可能有很多层,不过不要怕,无论多少层本质上都是一样的,我们只需要一层一层剥离开,分析结构,整个hoc结构和脉络就会清晰可见。吃透hoc也就易如反掌。

4 两种不同的高阶组件

常用的高阶组件有两种方式正向的属性代理反向的组件继承,两者之前有一些共性和区别。接下具体介绍两者区别,在第三部分会详细介绍具体实现。

正向属性代理

所谓正向属性代理,就是用组件包裹一层代理组件,在代理组件上,我们可以做一些,对源组件的代理操作。在fiber tree 上,先mounted代理组件,然后才是我们的业务组件。我们可以理解为父子组件关系,父组件对子组件进行一系列强化操作。

function HOC(WrapComponent){
   
   
    return class Advance extends React.Component{
   
   
       state={
   
   
           name:'alien'
       }
       render(){
   
   
           return <WrapComponent  {
   
    ...this.props } {
   
    ...this.state }  />
       }
    }
}

优点

  • ① 正常属性代理可以和业务组件低耦合,零耦合,对于条件渲染props属性增强,只负责控制子组件渲染和传递额外的props就可以,所以无须知道,业务组件做了些什么。所以正向属性代理,更适合做一些开源项目的hoc,目前开源的HOC基本都是通过这个模式实现的。
  • ② 同样适用于class声明组件,和function声明的组件。
  • ③ 可以完全隔离业务组件的渲染,相比反向继承,属性代理这种模式。可以完全控制业务组件渲染与否,可以避免反向继承带来一些副作用,比如生命周期的执行。
  • ④ 可以嵌套使用,多个hoc是可以嵌套使用的,而且一般不会限制包装HOC的先后顺序。

缺点

  • ① 一般无法直接获取业务组件的状态,如果想要获取,需要ref获取组件实例。

  • ② 无法直接继承静态属性。如果需要继承需要手动处理,或者引入第三方库。

例子:

class Index extends React.Component{
   
   
  render(){
   
   
    return <div> hello,world  </div>
  }
}
Index.say = function(){
   
   
  console.log('my name is alien')
}
function HOC(Component) {
   
   
  return class wrapComponent extends React.Component{
   
   
     render(){
   
   
       return <Component {
   
    ...this.props } {
   
    ...this.state } />
     }
  }
}
const newIndex =  HOC(Index) 
console.log(newIndex.say)

打印结果

29B0DA43-A037-473C-AD76-6550A3849CE8.jpg

反向继承

反向继承和属性代理有一定的区别,在于包装后的组件继承了业务组件本身,所以我们我无须在去实例化我们的业务组件。当前高阶组件就是继承后,加强型的业务组件。这种方式类似于组件的强化,所以你必要要知道当前

class Index extends React.Component{
   
   
  render(){
   
   
    return <div> hello,world  </div>
  }
}
function HOC(Component){
   
   
    return class wrapComponent extends Component{
   
    /* 直接继承需要包装的组件 */

    }
}
export default HOC(Index)

优点

  • ① 方便获取组件内部状态,比如stateprops ,生命周期,绑定的事件函数等
  • es6继承可以良好继承静态属性。我们无须对静态属性和方法进行额外的处理。
class Index extends React.Component{
   
   
  render(){
   
   
    return <div> hello,world  </div>
  }
}
Index.say = function(){
   
   
  console.log('my name is alien')
}
function HOC(Component) {
   
   
  return class wrapComponent extends Component{
   
   
  }
}
const newIndex =  HOC(Index) 
console.log(newIndex.say)

打印结果

3618DB30-8D9F-445A-8A01-69076A0B1E1D.jpg

缺点

  • ① 无状态组件无法使用。
  • ② 和被包装的组件强耦合,需要知道被包装的组件的内部状态,具体是做什么?
  • ③ 如果多个反向继承hoc嵌套在一起,当前状态会覆盖上一个状态。这样带来的隐患是非常大的,比如说有多个componentDidMount,当前componentDidMount会覆盖上一个componentDidMount。这样副作用串联起来,影响很大。

三 如何编写高阶组件

接下来我们来看看,如何编写一个高阶组件,你可以参考如下的情景,去编写属于自己的HOC

1 强化props

① 混入props

这个是高阶组件最常用的功能,承接上层的props,在混入自己的props,来强化组件。

有状态组件(属性代理)

function classHOC(WrapComponent){
   
   
    return class  Idex extends React.Component{
   
   
        state={
   
   
            name:'alien'
        }
        componentDidMount(){
   
   
           console.log('HOC')
        }
        render(){
   
   
            return <WrapComponent {
   
    ...this.props }  {
   
    ...this.state }   />
        }
    }
}
function Index(props){
   
   
  const {
   
    name } = props
  useEffect(()=>{
   
   
     console.log( 'index' )
  },[])
  return <div>
    hello,world , my name is {
   
    name }
  </div>
}

export default classHOC(Index)

有状态组件(属性代理)

同样也适用与无状态组件。

function functionHoc(WrapComponent){
   
   
    return function Index(props){
   
   
        const [ state , setState ] = useState({
   
    name :'alien'  })       
        return  <WrapComponent {
   
    ...props }  {
   
    ...state }   />
    }
}

效果

A6FC09B4-EAA0-4A5A-BA3A-F7F2A8407C75.jpg

② 抽离state控制更新

高阶组件可以将HOCstate的配合起来,控制业务组件的更新。这种用法在react-reduxconnect高阶组件中用到过,用于处理来自reduxstate更改,带来的订阅更新作用。

我们将上述代码进行改造。

function classHOC(WrapComponent){
   
   
  return class  Idex extends React.Component{
   
   
      constructor(){
   
   
        super()
        this.state={
   
   
          name:'alien'
        }
      }
      changeName(name){
   
   
        this.setState({
   
    name })
      }
      render(){
   
   
          return <WrapComponent {
   
    ...this.props }  {
   
    ...this.state } changeName={
   
   this.changeName.bind(this)  }  />
      }
  }
}
function Index(props){
   
   
  const [ value ,setValue ] = useState(null)
  const {
   
    name ,changeName } = props
  return <div>
    <div>   hello,world , my name is {
   
    name }</div>
    改变name <input onChange={
   
    (e)=> setValue(e.target.value)  }  />
    <button onClick={
   
    ()=>  changeName(value) }  >确定</button>
  </div>
}

export default classHOC(Index)

效果

屏幕录制2021-03-13 下午6.gif

2 控制渲染

控制渲染是高阶组件的一个很重要的特性,上边说到的两种高阶组件,都能完成对组件渲染的控制。具体实现还是有区别的,我们一起来探索一下。

2.1 条件渲染

① 基础 :动态渲染

对于属性代理的高阶组件,虽然不能在内部操控渲染状态,但是可以在外层控制当前组件是否渲染,这种情况应用于,权限隔离懒加载延时加载等场景。

实现一个动态挂载组件的HOC

function renderHOC(WrapComponent){
   
   
  return class Index  extends React.Component{
   
   
      constructor(props){
   
   
        super(props)
        this.state={
   
    visible:true }  
      }
      setVisible(){
   
   
         this.setState({
   
    visible:!this.state.visible })
      }
      render(){
   
   
         const {
   
     visible } = this.state 
         return <div className="box"  >
           <button onClick={
   
    this.setVisible.bind(this) } > 挂载组件 </button>
           {
   
    visible ? <WrapComponent {
   
    ...this.props } setVisible={
   
    this.setVisible.bind(this) }   />  : <div className="icon" ><SyncOutlined spin  className="theicon"  /></div> }
         </div>
      }
  }
}

class Index extends React.Component{
   
   
  render(){
   
   
    const {
   
    setVisible } = this.props
    return <div className="box" >
        <p>hello,my name is alien</p>
        <img  src='https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&fm=26&gp=0.jpg'   /> 
        <button onClick={
   
   () => setVisible()}  > 卸载当前组件 </button>
    </div>
  }
}
export default renderHOC(Index)

效果:

屏幕录制2021-03-13 下午9.gif

② 进阶 :分片渲染

是不是感觉不是很过瘾,为了让大家加强对HOC条件渲染的理解,我再做一个分片渲染+懒加载功能。为了让大家明白,我也是绞尽脑汁啊😂😂😂。

进阶:实现一个懒加载功能的HOC,可以实现组件的分片渲染,用于分片渲染页面,不至于一次渲染大量组件造成白屏效果

const renderQueue = []
let isFirstrender = false

const tryRender = ()=>{
   
   
  const render = renderQueue.shift()
  if(!render) return
  setTimeout(()=>{
   
   
    render() /* 执行下一段渲染 */
  },300)
} 
/* HOC */
function renderHOC(WrapComponent){
   
   
    return function Index(props){
   
   
      const [ isRender , setRender ] = useState(false)
      useEffect(()=>{
   
   
        renderQueue.push(()=>{
   
     /* 放入待渲染队列中 */
          setRender(true)
        })
        if(!isFirstrender) {
   
   
          tryRender() /**/
          isFirstrender = true
        }
      },[])
      return isRender ? <WrapComponent tryRender={
   
   tryRender}  {
   
    ...props }  /> : <div className='box' ><div className="icon" ><SyncOutlined   spin /></div></div>
    }
}
/* 业务组件 */
class Index extends React.Component{
   
   
  componentDidMount(){
   
   
    const {
   
    name , tryRender} = this.props
    /* 上一部分渲染完毕,进行下一部分渲染 */
    tryRender()
    console.log( name+'渲染')
  }
  render(){
   
   
    return <div>
        <img src="https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&amp;fm=26&amp;gp=0.jpg" />
    </div>
  }
}
/* 高阶组件包裹 */
const Item = renderHOC(Index)

export default () => {
   
   
  return <React.Fragment>
      <Item name="组件一" />
      <Item name="组件二" />
      <Item name="组件三" />
  </React.Fragment>
}

效果

fenload.gif

大致流程,初始化的时候,HOC中将渲染真正组件的渲染函数,放入renderQueue队列中,然后初始化渲染一次,接下来,每一个项目组件,完成 didMounted 状态后,会从队列中取出下一个渲染函数,渲染下一个组件, 一直到所有的渲染任务全部执行完毕,渲染队列清空,有效的进行分片的渲染,这种方式对海量数据展示,很奏效。

HOC实现了条件渲染-分片渲染的功能,实际条件渲染理解起来很容易,就是通过变量,控制是否挂载组件,从而满足项目本身需求,条件渲染可以演变成很多模式,我这里介绍了条件渲染的二种方式,希望大家能够理解精髓所在。

③ 进阶:异步组件(懒加载)

不知道大家有没有用过dva,里面的dynamic就是应用HOC模式实现的组件异步加载,我这里简化了一下,提炼核心代码,如下:

/* 路由懒加载HOC */
export default function AsyncRouter(loadRouter) {
   
   
  return class Content extends React.Component {
   
   
    state = {
   
   Component: null}
    componentDidMount() {
   
   
      if (this.state.Component) return
      loadRouter()
        .then(module => module.default)
        .then(Component => this.setState({
   
   Component},
         ))
    }
    render() {
   
   
      const {
   
   Component} = this.state
      return Component ? <Component {
   
   
      ...this.props
      }
      /> : null
    }
  }
}

使用

const Index = AsyncRouter(()=>import('../pages/index'))

hoc还可以配合其他API,做一下衍生的功能。如上配合import实现异步加载功能。HOC用起来非常灵活,

④ 反向继承 : 渲染劫持

HOC反向继承模式,可以实现颗粒化的渲染劫持,也就是可以控制基类组件的render函数,还可以篡改props,或者是children,我们接下来看看,这种状态下,怎么使用高阶组件。


const HOC = (WrapComponent) =>
  class Index  extends WrapComponent {
   
   
    render() {
   
   
      if (this.props.visible) {
   
   
        return super.render()
      } else {
   
   
        return <div>暂无数据</div>
      }
    }
  }

⑤ 反向继承:修改渲染树

修改渲染状态(劫持render替换子节点)

class Index extends React.Component{
   
   
  render(){
   
   
    return <div>
       <ul>
         <li>react</li>
         <li>vue</li>
         <li>Angular</li>
       </ul>
    </div>
  }
}

function HOC (Component){
   
   
  return class Advance extends Component {
   
   
    render() {
   
   
      const element = super.render()
      const otherProps = {
   
   
        name:'alien'
      }
      /* 替换 Angular 元素节点 */
      const appendElement = React.createElement('li' ,{
   
   } , `hello ,world , my name  is ${
     
      otherProps.name }` )
      const newchild =  React.Children.map(element.props.children.props.children,(child,index)=>{
   
   
           if(index === 2) return appendElement
           return  child
      }) 
      return  React.cloneElement(element, element.props, newchild)
    }
  }
}
export  default HOC(Index)

效果

40D6BF30-9B4C-4EC9-B089-1E757DAC15DF.jpg

我们用劫持渲染的方式,来操纵super.render()后的React.element元素,然后配合 createElement , cloneElement , React.Childrenapi,可以灵活操纵,真正的渲染react.element,可以说是偷天换日,不亦乐乎。

2.2节流渲染

hoc除了可以进行条件渲染渲染劫持功能外,还可以进行节流渲染,也就是可以优化性能,具体怎么做,请跟上我的节奏往下看。

① 基础: 节流原理

hoc可以配合hooksuseMemoAPI配合使用,可以实现对业务组件的渲染控制,减少渲染次数,从而达到优化性能的效果。如下案例,我们期望当且仅当num改变的时候,渲染组件,但是不影响接收的props。我们应该这样写我们的HOC

function HOC (Component){
   
   
     return function renderWrapComponent(props){
   
   
       const {
   
    num } = props
       const RenderElement = useMemo(() =>  <Component {
   
   ...props}  /> ,[ num ])
       return RenderElement
     }
}
class Index extends React.Component{
   
   
  render(){
   
   
     console.log(`当前组件是否渲染`,this.props)
     return <div>hello,world, my name is alien </div>
  }
}
const IndexHoc = HOC(Index)

export default ()=> {
   
   
    const [ num ,setNumber ] = useState(0)
    const [ num1 ,setNumber1 ] = useState(0)
    const [ num2 ,setNumber2 ] = useState(0)
    return <div>
        <IndexHoc  num={
   
    num } num1={
   
   num1} num2={
   
    num2 }  />
        <button onClick={
   
   () => setNumber(num + 1) } >num++</button>
        <button onClick={
   
   () => setNumber1(num1 + 1) } >num1++</button>
        <button onClick={
   
   () => setNumber2(num2 + 1) } >num2++</button>
    </div>
}

效果:

rend1.gif

如图所示,当我们只有点击 num++时候,才重新渲染子组件,点击其他按钮,只是负责传递了props,达到了期望的效果。

② 进阶:定制化渲染流

思考:🤔上述的案例只是介绍了原理,在实际项目中,是量化生产不了的,原因是,我们需要针对不同props变化,写不同的HOC组件,这样根本起不了Hoc真正的用途,也就是HOC产生的初衷。所以我们需要对上述hoc进行改造升级,是组件可以根据定制化方向,去渲染组件。也就是Hoc生成的时候,已经按照某种契约去执行渲染。

function HOC (rule){
   
   
     return function (Component){
   
   
        return function renderWrapComponent(props){
   
   
          const dep = rule(props)
          const RenderElement = useMemo(() =>  <Component {
   
   ...props}  /> ,[ dep ])
          return RenderElement
        }
     }
}
/* 只有 props 中 num 变化 ,渲染组件  */
@HOC( (props)=> props['num'])
class IndexHoc extends React.Component{
   
   
  render(){
   
   
     console.log(`组件一渲染`,this.props)
     return <div> 组件一 : hello,world </div>
  }
}

/* 只有 props 中 num1 变化 ,渲染组件  */
@HOC((props)=> props['num1'])
class IndexHoc1 extends React.Component{
   
   
  render(){
   
   
     console.log(`组件二渲染`,this.props)
     return <div> 组件二 : my name is alien </div>
  }
}
export default ()=> {
   
   
    const [ num ,setNumber ] = useState(0)
    const [ num1 ,setNumber1 ] = useState(0)
    const [ num2 ,setNumber2 ] = useState(0)
    return <div>
        <IndexHoc  num={
   
    num } num1={
   
   num1} num2={
   
    num2 }  />
        <IndexHoc1  num={
   
    num } num1={
   
   num1} num2={
   
    num2 }  />
        <button onClick={
   
   () => setNumber(num + 1) } >num++</button>
        <button onClick={
   
   () => setNumber1(num1 + 1) } >num1++</button>
        <button onClick={
   
   () => setNumber2(num2 + 1) } >num2++</button>
    </div>
}

效果

hoc2.gif

完美实现了效果。这用高阶组件模式,可以灵活控制React组件层面上的,props数据流更新流,优秀的高阶组件有 mobxobserver ,inject , react-redux中的connect,感兴趣的同学,可以抽时间研究一下。

3 赋能组件

高阶组件除了上述两种功能之外,还可以赋能组件,比如加一些额外生命周期劫持事件监控日志等等。

3.1 劫持原型链-劫持生命周期,事件函数

① 属性代理实现

function HOC (Component){
   
   
  const proDidMount = Component.prototype.componentDidMount 
  Component.prototype.componentDidMount = function(){
   
   
     console.log('劫持生命周期:componentDidMount')
     proDidMount.call(this)
  }
  return class wrapComponent extends React.Component{
   
   
      render(){
   
   
        return <Component {
   
   ...this.props}  />
      }
  }
}
@HOC
class Index extends React.Component{
   
   
   componentDidMount(){
   
   
     console.log('———didMounted———')
   }
   render(){
   
   
     return <div>hello,world</div>
   }
}

效果

A04A37C8-71CF-4DFD-BD59-E741DCC35EF4.jpg

② 反向继承实现

反向继承,因为在继承原有组件的基础上,可以对原有组件的生命周期事件进行劫持,甚至是替换。

function HOC (Component){
   
   
  const didMount = Component.prototype.componentDidMount
  return class wrapComponent extends Component{
   
   
      componentDidMount(){
   
   
        console.log('------劫持生命周期------')
        if (didMount) {
   
   
           didMount.apply(this) /* 注意 `this` 指向问题。 */
        }
      }
      render(){
   
   
        return super.render()
      }
  }
}

@HOC
class Index extends React.Component{
   
   
   componentDidMount(){
   
   
     console.log('———didMounted———')
   }
   render(){
   
   
     return <div>hello,world</div>
   }
}

3.2 事件监控

HOC还可以对原有组件进行监控。比如对一些事件监控错误监控事件监听等一系列操作。

① 组件内的事件监听

接下来,我们做一个HOC,只对组件内的点击事件做一个监听效果。


function ClickHoc (Component){
   
   
  return  function Wrap(props){
   
   
    const dom = useRef(null)
    useEffect(()=>{
   
   
     const handerClick = () => console.log('发生点击事件') 
     dom.current.addEventListener('click',handerClick)
     return () => dom.current.removeEventListener('click',handerClick)
    },[])
    return  <div ref={
   
   dom}  ><Component  {
   
   ...props} /></div>
  }
}

@ClickHoc
class Index extends React.Component{
   
   
   render(){
   
   
     return <div  className='index'  >
       <p>hello,world</p>
       <button>组件内部点击</button>
    </div>
   }
}
export default ()=>{
   
   
  return <div className='box'  >
     <Index />
     <button>组件外部点击</button>
  </div>
}

效果

click.gif

3 ref助力操控组件实例

对于属性代理我们虽然不能直接获取组件内的状态,但是我们可以通过ref获取组件实例,获取到组件实例,就可以获取组件的一些状态,或是手动触发一些事件,进一步强化组件,但是注意的是:class声明的有状态组件才有实例,function声明的无状态组件不存在实例。

① 属性代理-添加额外生命周期

我们可以针对某一种情况, 给组件增加额外的生命周期,我做了一个简单的demo,监听number改变,如果number改变,就自动触发组件的监听函数handerNumberChange
具体写法如下

function Hoc(Component){
   
   
  return class WrapComponent extends React.Component{
   
   
      constructor(){
   
   
        super()
        this.node = null
      }
      UNSAFE_componentWillReceiveProps(nextprops){
   
   
          if(nextprops.number !== this.props.number ){
   
   
            this.node.handerNumberChange  &&  this.node.handerNumberChange.call(this.node)
          }
      }
      render(){
   
   
        return <Component {
   
   ...this.props} ref={
   
   (node) => this.node = node }  />
      }
  }
}
@Hoc
class Index extends React.Component{
   
   
  handerNumberChange(){
   
   
      /* 监听 number 改变 */
  }
  render(){
   
   
    return <div>hello,world</div>
  }
}

这种写法有点不尽人意,大家不要着急,在第四部分,源码实战中,我会介绍一种更好的场景。方便大家理解Hoc对原有组件的赋能。

4 总结

上面我分别按照hoc主要功能,强化props控制渲染赋能组件 三个方向对HOC编写做了一个详细介绍,和应用场景的介绍,目的让大家在理解高阶组件的时候,更明白什么时候会用到?,怎么样去写?`
里面涵盖的知识点我总一个总结。

对于属性代理HOC,我们可以:

  • 强化props & 抽离state。
  • 条件渲染,控制渲染,分片渲染,懒加载。
  • 劫持事件和生命周期
  • ref控制组件实例
  • 添加事件监听器,日志

对于反向代理的HOC,我们可以:

  • 劫持渲染,操纵渲染树
  • 控制/替换生命周期,直接获取组件状态,绑定事件。

每个应用场景,我都举了例子🌰🌰,大家可以结合例子深入了解一下其原理和用途。

相关文章
|
1天前
|
前端开发 JavaScript 开发者
React 按钮组件 Button
本文介绍了 React 中按钮组件的基础概念,包括基本的 `&lt;button&gt;` 元素和自定义组件。详细探讨了事件处理、参数传递、状态管理、样式设置和可访问性优化等常见问题及其解决方案,并提供了代码示例。帮助开发者避免易错点,提升按钮组件的使用体验。
99 77
|
2天前
|
前端开发 UED 开发者
React 对话框组件 Dialog
本文详细介绍了如何在 React 中实现一个功能完备的对话框组件(Dialog),包括基本用法、常见问题及其解决方案,并通过代码案例进行说明。从安装依赖到创建组件、添加样式,再到解决关闭按钮失效、背景点击无效、键盘导航等问题,最后还介绍了如何添加动画效果和处理异步关闭操作。希望本文能帮助你在实际开发中更高效地使用 React 对话框组件。
96 75
|
7天前
|
前端开发 Java API
React 进度条组件 ProgressBar 详解
本文介绍了如何在 React 中创建进度条组件,从基础实现到常见问题及解决方案,包括动态更新、状态管理、性能优化、高级动画效果和响应式设计等方面,帮助开发者构建高效且用户体验良好的进度条。
35 18
|
22天前
|
存储 前端开发 测试技术
React组件的最佳实践
React组件的最佳实践
|
20天前
|
前端开发 API 开发者
React 文件上传组件 File Upload
本文详细介绍了如何在 React 中实现文件上传组件,从基础的文件选择和上传到服务器,再到解决文件大小、类型限制、并发上传等问题,以及实现多文件上传、断点续传和文件预览等高级功能,帮助开发者高效构建可靠的应用。
48 12
|
15天前
|
存储 前端开发 JavaScript
React 表单输入组件 Input:常见问题、易错点及解决方案
本文介绍了在 React 中使用表单输入组件 `Input` 的基础概念,包括受控组件与非受控组件的区别及其优势。通过具体代码案例,详细探讨了创建受控组件、处理多个输入字段、输入验证和格式化的方法,并指出了常见易错点及避免方法,旨在提升表单的健壮性和用户体验。
27 4
|
22天前
|
前端开发 JavaScript API
React 文件下载组件 File Download
本文介绍了在React中实现文件下载组件的方法,包括使用`a`标签和JavaScript动态生成文件,解决了文件路径、文件类型、大文件下载及文件名乱码等问题,并展示了使用第三方库`file-saver`和生成CSV文件的高级用法。
35 6
|
19天前
|
前端开发 JavaScript API
React 文件下载组件:File Download
本文详细介绍了如何在React应用中实现文件下载组件,包括基本概念、实现步骤和代码示例。同时,探讨了常见问题如文件类型不匹配、文件名乱码等及其解决方法,旨在提升用户体验和代码可维护性。
38 2
|
23天前
|
存储 前端开发 JavaScript
React 文件上传组件 File Upload
本文介绍了如何在 React 中实现文件上传组件,包括基本的概念、实现步骤、常见问题及解决方案。通过 `&lt;input type=&quot;file&quot;&gt;` 元素选择文件,使用 `fetch` 发送请求,处理文件类型和大小限制,以及多文件上传和进度条显示等高级功能,帮助开发者构建高效、可靠的文件上传组件。
65 2
|
24天前
|
存储 前端开发
在React框架中,如何使用对象来管理组件的状态
在React中,组件状态通过`state`对象管理,利用`setState`方法更新状态。状态变化触发组件重新渲染,实现UI动态更新。对象结构清晰,便于复杂状态管理。