聊聊React的高阶组件

路漫漫其修远兮,吾将上下而求索。— 屈原《离骚》

写在前面

高阶组件不是React API的一部分,而是一种用来复用组件逻辑而衍生出来的一种技术

A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.

在讨论高阶组件组件之前,我们先来聊聊高阶函数

高阶函数

之前我写过一篇文章 函数的柯里化与Redux中间件及applyMiddleware源码分析,在这边文章里我已经讲过了什么是高阶函数,我们再来回顾一下,所谓的高阶函数就是:

  • 函数可以作为参数
  • 函数可以作为返回值

如:

const debounce = (fn, delay) => {
    let timeId = null
    return () => {
        timeId && clearTimeout(timeId)
        timeId = setTimeout(fn, delay)
    }
}
复制代码

高阶函数的应用有很多,函数去抖,函数节流,bind函数,函数柯里化,map,Promise的then函数等

高阶组件

高阶组件的定义和高阶函数有点像,但是要注意以下:

  • 传入一个组件
  • 返回一个新组件
  • 是一个函数

是不是感觉和高阶函数很像,没错

总之,高阶组件就是包裹(Wrapped)传入的React组件,经过一系列处理,返回一个相对增强(Enhanced)的组件

react-redux中的connect函数就是高阶组件很好的一个应用

如何编写一个高阶组件

下面通过一个例子来帮助大家编写属于自己的高阶组件

现在我们有两个组件,一个是UI组件Demo,用来显示文本,一个是withHeader,它接受一个组件,然后返回一个新的组件,只不过给传入的组件加上了一个标题,这个withHeader就是高阶组件

class Demo extends Component {
  render() {
    return (
      <div>
        我是一个普通组件1
      </div>
    )
  }
}

const withHeader = WrappedComponent => {
  class HOC extends Component {
    render() {
      return (
        <div>
          <h1 className='demo-header'>我是标题</h1>
          <WrappedComponent {...this.props}/>
        </div>
      )
    }
  }
  return HOC
}

const EnhanceDemo = withHeader(Demo)
复制代码

结果如下:

HOC组件就是高阶组件,它包裹了传入的Demo组件,并给他添加了一个标题

假设有三个Demo组件,Demo1,Demo2,Demo3它们之间的区别就是组件的内容不一样(这样做是方便做演示),它们都用HOC进行包裹了,结果发现包裹之后的组件名称都为HOC,这时候我们需要区分包裹之后的三个高阶组件,

给HOC添加静态displayName属性

const getDisplayName = component => {
  return component.displayName || component.name || 'Component'
}

const withHeader = WrappedComponent => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return (
        <div>
          <h1 className='demo-header'>我是标题</h1>
          <WrappedComponent {...this.props}/>
        </div>
      )
    }
  }
  return HOC
}
复制代码

再看看三个高阶组件的名称都不一样了,但是我们想让标题不写死,而是可以动态传入可以吗?当然是可以的,我们可以借助函数的柯里化实现,我们对withHeader改进下

const withHeader = title => WrappedComponent => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return (
        <div>
          <h1 className='demo-header'>{title}</h1>
          <WrappedComponent {...this.props}/>
        </div>
      )
    }
  }
  return HOC
}

const EnhanceDemo1 = withHeader('Demo1')(Demo1)
const EnhanceDemo2 = withHeader('Demo2')(Demo2)
const EnhanceDemo3 = withHeader('Demo3')(Demo3)
复制代码

结果如下:

我们可以借助ES7的装饰器来让我们的写法更简洁

@withHeader('Demo1')
class Demo1 extends Component {
  render() {
    return (
      <div>
        我是一个普通组件1
      </div>
    )
  }
}

@withHeader('Demo2')
class Demo2 extends Component {
  render() {
    return (
      <div>
        我是一个普通组件2
      </div>
    )
  }
}

@withHeader('Demo3')
class Demo3 extends Component {
  render() {
    return (
      <div>
        我是一个普通组件3
      </div>
    )
  }
}

class App extends Component {
  render() {
    return (
      <Fragment>
        <Demo1 />
        <Demo2 />
        <Demo3 />
      </Fragment>
    )
  }
}
复制代码

关于装饰器是什么及怎么使用,大家可自行查阅,后面我也专门写一遍文章来讲解它

到此为止,我们已经掌握了如何编写一个高阶组件,但是还没完

两种高阶组件的实现方式

下面来说说高阶组件的两种实现方式:

  • 属性代理
  • 反向继承

属性代理

属性代理是最常见的方式,上面讲的例子就是基于这种方式,只不过我们还可以写的更完善些,我们可以在HOC中自定义一些属性,然后和新生成的属性一起传给被包裹的组件,如下:

const withHeader = title => WrappedComponent => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      const newProps = {
        id: Math.random().toString(36).substring(2).toUpperCase()
      }
      return (
        <div>
          <h1 className='demo-header'>{title}</h1>
          <WrappedComponent {...this.props} {...newProps}/>
        </div>
      )
    }
  }
  return HOC
}

@withHeader('标题')
class Demo extends Component {
  render() {
    return (
      <div style={this.props}>
        { this.props.children }
      </div>
    )
  }
}

class App extends Component {
  render() {
    return (
      <Fragment>
        <Demo color='blue'>我是一个普通组件</Demo>
      </Fragment>
    )
  }
}
复制代码

显示如下:

对上面的高阶组件和被包裹组件进行了改进,高阶组件内部可以生成一个id属性并传入被包裹组件中,同时高阶组件外部也可以接受属性并传入被包裹组件

反向继承

咱们对上面的例子又做了一番修改:

我们让HOC继承wrappedComponent,这样我们的HOC就拥有了wrappedComponent中定义的属性和方法了,如state,props,生命周期函数

const withHeader = title => WrappedComponent => {
  class HOC extends WrappedComponent {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return (
        <div>
          <h1 className='demo-header'>{title}</h1>
          { super.render() }
        </div>
      )
    }
    componentDidMount() {
      this.setState({
        innerText: '我的值变成了2'
      })
    }
  }
  return HOC
}
复制代码

注意,HOC中的render函数,要调用父类的render函数,需要用到super关键字

相应的Demo组件也做了修改

@withHeader('标题')
class Demo extends Component {
  constructor(props) {
    super(props)
    this.state = {
      innerText: '我的初始值是1'
    }
  }
  render() {
    return (
      <div style={this.props}>
        { this.state.innerText }
      </div>
    )
  }
}

class App extends Component {
  render() {
    return (
      <Fragment>
        <Demo color='blue'></Demo>
      </Fragment>
    )
  }
}
复制代码

最后显示如下:

注意高阶组件内部没有了Demo组件,而是用原生的HTML标签代替,可以对比上面的图看出差异,为什么没有了Demo组件?因为我们是调用父类的render函数,而不是直接使用React组件

因为这种方式是让我们的HOC继承WrappedComponent,换句话也就是WrappedComponent被HOC继承,所以称为反向继承

容易踩的坑

两种继承方式说完了,再说下书写高阶组件容易犯错的地方,这也是官方文档需要我们注意的

不要在render函数中使用高阶组件

这个需要大家对diff算法有所了解,如果从 render 返回的组件等同于之前render函数返回的组件,React将会迭代地通过diff算法更新子树到新的子树。如果不相等,则先前的子树将会完全卸载。

而我们如果在render函数中使用高阶组件,每次都会生成一个新的高阶组件实例,这样每次都会使对应的组件树重新卸载,当然这样肯定会有性能问题,但是不仅仅是性能问题,它还会造成组件的state和children全部丢失,这个才是致命的

静态方法需手动复制

当我们在WrappedComponent定义的静态方法,在高阶组件实例上是找不到的

const withHeader = title => WrappedComponent => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return (
        <div>
          <h1 className='demo-header'>{title}</h1>
          <WrappedComponent>
            { this.props.children }
          </WrappedComponent>
        </div>
      )
    }
  }
  return HOC
}

class Demo extends Component {
  static hello() {
    console.log('22')
  }
  render() {
    return (
      <div style={this.props}>
        { this.props.children }
      </div>
    )
  }
}

const WithHeaderDemo = withHeader('标题')(Demo)
WithHeaderDemo.hello()
复制代码

我们有两种办法可以解决:

  • 手动复制WrappedComponent的static方法到高阶组件上
  • 使用hoistNonReactStatic

第一种方法需要我们知道WrappedComponent上有哪些static方法,有一定的局限性,通常我们使用第二种方法,但是需要我们安装一个第三方库:hoist-non-react-statics

import hoistNonReactStatic from 'hoist-non-react-statics'

const withHeader = title => WrappedComponent => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return (
        <div>
          <h1 className='demo-header'>{title}</h1>
          <WrappedComponent>
            { this.props.children }
          </WrappedComponent>
        </div>
      )
    }
  }
  hoistNonReactStatic(HOC, WrappedComponent)
  return HOC
}
复制代码

这样就不会报错了

Ref不会被传递

高阶组件可以把所有属性传递给被包裹组件,但是ref除外,因为ref不是一个真正的属性,React 对它进行了特殊处理, 如果你向一个由高阶组件创建的组件的元素添加ref应用,那么ref指向的是最外层容器组件实例的,而不是包裹组件。

看一个例子就明白了

class App extends Component {
  render() {
    const WrappedComponentRef = React.createRef()
    this.WrappedComponentRef = WrappedComponentRef
    return (
      <Fragment>
        <WithHeaderDemo color='blue' ref={WrappedComponentRef}>
          33333
        </WithHeaderDemo>
      </Fragment>
    )
  }
  componentDidMount() {
    console.log(this.WrappedComponentRef.current)
  }
}
复制代码

结果打印的信息如下:

我们的本意是把ref传递给内层包裹的WrappedComponent,结果打印的确是外层的HOC,我们再去看看React组件树的信息

ref作为了HOC的属性并没有传递到内部去,我想肯定是React对ref做了特殊的处理了,怎么解决呢?简单,换个名字不就可以了,我使用的是_ref

const withHeader = title => WrappedComponent => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`
    render() {
      return (
        <div>
          <h1 className='demo-header'>{title}</h1>
          <WrappedComponent ref={this.props._ref}>
            { this.props.children }
          </WrappedComponent>
        </div>
      )
    }
  }
  hoistNonReactStatic(HOC, WrappedComponent)
  return HOC
}

class App extends Component {
  render() {
    const WrappedComponentRef = React.createRef()
    this.WrappedComponentRef = WrappedComponentRef
    return (
      <Fragment>
        <WithHeaderDemo color='blue' _ref={WrappedComponentRef}>
          33333
        </WithHeaderDemo>
      </Fragment>
    )
  }
  componentDidMount() {
    console.log(this.WrappedComponentRef.current)
  }
}

复制代码

再来看看我们的打印结果

对应的React组件树的情况

完美解决!

最后

本文只是简单的介绍了下高阶组件的书写和其两种实现方式,及一些要避免的坑,如果想对高阶组件有个更系统的了解推荐去阅读 React官方文档的高阶组件,还有可以阅读一些源码:如react-redux的connect,antd的Form.create

React学习之路很有很长

你们的打赏是我写作的动力

微信
支付宝

猜你喜欢

转载自juejin.im/post/5b7666b1e51d45560c1554a3