路漫漫其修远兮,吾将上下而求索。— 屈原《离骚》
写在前面
高阶组件不是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学习之路很有很长
你们的打赏是我写作的动力