什么是 React 高阶组件,请举例说明一下

1,408 阅读5分钟

前情提要


各位好,最近在学习了 React 高阶组件的相关知识后,做一篇学习笔记。

高阶组件是一种很好的模式,许多 React 库都使用了高阶组件,那么高阶组件究竟能做什么呢?它又有哪些局限呢?今天就让我们好好了解一下。

什么是高阶组件


高阶组件就是一个 React 组件包裹着另外一个 React 组件

通常我们定义一个函数来实现这种模式,这个函数接受一个 React 组件作为参数,最后返回一个新的React 组件。它的函数签名可以用类似 haskell 的伪代码表示:

hocFactory:: W: React.Component => E: React.Component

其中 W (WrappedComponent) 指被包裹的 React.Component,E (EnhancedComponent) 指返回类型为 React.Component 的新的 HOC。

高阶组件中,“包裹”的方式有两种:

一、属性代理(Props Proxy): HOC 对传给 WrappedComponent 的 props 进行操作

二、反向继承(Inheritance Inversion):HOC 继承 WrappedComponent

属性代理(Props Proxy)


先让我们来看一下最简单的属性代理实现:

function ppHOC(WrappedComponent) {  
  return class PP extends React.Component {    
    render() {      
      return <WrappedComponent {...this.props}/>    
    }  
  } 
}

从上面可以看到,ppHOC 接受了一个 WrappedComponent 作为参数,然后返回了一个新 PP 类组件。在 PP 组件的 render 方法中,返回了 WrappedComponent 的 React Element,并且传入了 PP 组件 接收到的 props,这就是名字 Props Proxy 的由来。

那么 Props Proxy 可以做什么呢?

操作 props

在 HOC 中可以读取、添加、编辑、删除传给 WrappedComponent 的 props。

这句什么意思?举个例子:

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        user: currentLoggedInUser
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

从上面例子可以看出,我们可以在 ppHOC 组件中为 WrappedComponent 组件添加额外的 props。 同时,如果在外部父组件传入 props 也会先经过 ppHOC 组件,我们可以对其进行过滤和编辑。

当删除或者编辑重要的 props 时需要注意,避免破坏 WrappedComponent 组件。

通过 Ref 访问到组件实例

在 React 中,我们可以通过 ref 属性访问到组件实例,那么同理,在高阶组件中,我们也可以通过 ref 属性访问到 WrappedComponent 组件实例

function refsHOC(WrappedComponent) {
  return class RefsHOC extends React.Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method()
    }

    render() {
      const props = Object.assign({}, this.props, {ref: this.proc})
      return <WrappedComponent {...props}/>
    }
  }
}

Ref 的回调函数会在 WrappedComponent 渲染时执行,你就可以得到 WrappedComponent 的引用。这可以用来读取/添加实例的 props ,调用实例的方法。

用其他元素包裹 WrappedComponent

这是高阶组件最常见作用,可以用于封装样式、布局或别的目的。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      return (
        <div style={{display: 'block'}}>
          <WrappedComponent {...this.props}/>
        </div>
      )
    }
  }
}

反向继承(Inheritance Inversion)


我们来看一下反向继承最简单的实现:

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

在这种方式中可以看到,WrappedComponent 被 Enhancer 继承了,而不是 WrappedComponent 继承了 Enhancer,它们的关系看上去被反转(inverse)了。

Inheritance Inversion 允许 HOC 通过 this 访问到 WrappedComponent,意味着它可以访问到 stateprops组件生命周期方法render 方法。

那么 Inheritance Inversion 可以做什么呢?

渲染劫持

先看一个例子:

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render()
      } else {
        return null
      }
    }
  }
}

我们可以看到,HOC 控制着 WrappedComponent 的渲染输出,可以用它做各种各样的事。

通过渲染劫持你可以:

  • 在由 render 输出的任何 React 元素中读取、添加、编辑、删除 props
  • 读取和修改由 render 输出的 React 元素树
  • 有条件地渲染元素树
  • 把样式包裹进元素树(就像在 Props Proxy 中的那样)

需要注意的是,你不能编辑或添加 WrappedComponent 实例的 props,因为 React 组件不能编辑它接收到的 props,但你可以修改由 render 方法返回的组件的 props。

操作 state

HOC 可以读取、编辑和删除 WrappedComponent 实例的 state,如果需要,也可以给它添加更多的 state。但需要注意的是,这里有可能破坏 WrappedComponent 实例的 state。

export function IIHOCDEBUGGER(WrappedComponent) {
  return class II extends WrappedComponent {
    render() {
      return (
        <div>
          <h2>HOC Debugger Component</h2>
          <p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre>
          <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
          {super.render()}
        </div>
      )
    }
  }
}

从上面例子看到,在高阶组件 II 中,并没定义 props 和 state,但是由于该组件继承于 WrappedComponent 组件,所以通过 this 可以访问到 WrappedComponent 上的 props 和 state。

总结

属性代理(Props Proxy)

优点

① 正常属性代理可以和业务组件低耦合,零耦合,对于条件渲染和props属性增强,只负责控制子组件渲染和传递额外的props就可以,所以无须知道,业务组件做了些什么。所以正向属性代理,更适合做一些开源项目的hoc,目前开源的HOC基本都是通过这个模式实现的。

② 同样适用于class声明组件,和function声明的组件。

③ 可以完全隔离业务组件的渲染,相比反向继承,属性代理这种模式。可以完全控制业务组件渲染与否,可以避免反向继承带来一些副作用,比如生命周期的执行。

④ 可以嵌套使用,多个hoc是可以嵌套使用的,而且一般不会限制包装HOC的先后顺序。

缺点

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

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

反向继承(Inheritance Inversion)

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

① 方便获取组件内部状态,比如state,props ,生命周期,绑定的事件函数等

② es6继承可以良好继承静态属性。我们无须对静态属性和方法进行额外的处理。

缺点

① 无状态组件无法使用。

② 和被包装的组件强耦合,需要知道被包装的组件的内部状态,具体是做什么?

③ 如果多个反向继承hoc嵌套在一起,当前状态会覆盖上一个状态。这样带来的隐患是非常大的,比如说有多个componentDidMount,当前componentDidMount会覆盖上一个componentDidMount。这样副作用串联起来,影响很大。