天天看点

React 高阶组件HOC

概述

高阶组件( higher-order component ,HOC )是 React 中复用组件逻辑的一种进阶技巧,通俗的讲,高阶组件就是一个 React 组件包裹着另外一个 React 组件。它本身并不是 React 的 API,而是一种 React 组件设计理念,众多的 React 库已经证明了它的价值,例如耳熟能详的 react-redux。

高级组件使用函数来实现,基本上是一个类工厂,它的函数签名可以用类似 haskell 的伪代码表示:

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

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

高阶函数是把函数作为参数传入到函数中并返回一个新的函数。倘若我们把函数替换为组件,就是高阶组件,实现上如下:

const EnhancedComponent = higherOrderComponent(WrappedComponent);      

HOC 工厂实现

高阶组件通常有两种实现方式:一种是 Props Proxy,另一种是Inheritance Inversion。

  • Props Proxy: HOC 对传给 WrappedComponent W 的 porps 进行操作,并将提取 WrappedComponent state 以及使用其他元素来包裹 WrappedComponent。
  • Inheritance Inversion: HOC 继承 WrappedComponent W。

Props Proxy 作为一层代理,会发生隔离,因此传入 WrappedComponent 的 ref 将无法访问到其本身,需在 Props Proxy 内完成中转。

Props Proxy

Props Proxy的简单实现。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component
    // 实现 HOC 不同的命名
    static displayName = `HOC(${WrappedComponent.displayName})`;

    getWrappedInstance() {
      return this.wrappedInstance;
    }

    // 实现 ref 的访问
    setWrappedInstance(ref) {
      this.wrappedInstance = ref;
    }

    render() {
      return <WrappedComponent {
        ...this.props,
        ref: this.setWrappedInstance.bind(this),
      } />
    }
  }
}      

HOC 在 render 方法中 返回 了一个 WrappedComponent 类型的 React Element。我们还传入了 HOC 接收到的 props,这就是名字 Props Proxy 的由来。

Props Proxy 的作用

使用 Props Proxy 可以做什么呢?常见的有以下作用:

  1. 操作 props
  2. 通过 Refs 访问到组件实例
  3. 提取 state
  4. 用其他元素包裹 WrappedComponent

1, 操作 props

你可以读取、添加、编辑、删除传给 WrappedComponent 的 props。当删除或者编辑重要的 props 时要小心,你可能应该通过命名空间确保高阶组件的 props 不会破坏 WrappedComponent。

例如:添加一个新的 props:

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

2, 通过 Refs 访问到组件实例

你可以通过引用(ref)访问到 this (WrappedComponent 的实例),但为了得到引用,WrappedComponent 还需要一个初始渲染,意味着你需要在 HOC 的 render 方法中返回 WrappedComponent 元素,让 React 开始它的一致化处理,你就可以得到 WrappedComponent 的实例的引用。这个在React 异步开发中会经常用到。

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

    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
      return      

Ref 的回调函数会在 WrappedComponent 渲染时执行,你就可以得到 WrappedComponent 的引用。

3, 提取 state

你可以通过传入 props 和回调函数把 state 提取出来,类似于 smart component 与 dumb component。

例如,提取了 input 的 value 和 onChange 方法。

@ppHOC
class Example extends React.Component
  render() {
    return <input name="name" {...this.props.name}/>
  }
}      

4, 包裹 WrappedComponent

为了封装样式、布局或别的目的,你可以用其它组件和元素包裹 WrappedComponent。

例如,使用包裹样式:

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

Inheritance Inversion

另一种是 Inheritance Inversion,HOC 类继承了 WrappedComponent,意味着可以访问到 WrappedComponent 的 state、props、生命周期和 render 等方法。如果在 HOC 中定义了与 WrappedComponent 同名方法,将会发生覆盖,就必须手动通过 super 进行调用了。通过完全操作 WrappedComponent 的 render 方法返回的元素树,可以真正实现渲染劫持。这种方案依然是继承的思想,对于 WrappedComponent 也有较强的侵入性,因此并不常见。

例如,下面的代码:

function ppHOC(WrappedComponent) {
  return class ExampleEnhance extends WrappedComponent {
    ...
    componentDidMount() {
      super.componentDidMount();
    }
    componentWillUnmount() {
      super.componentWillUnmount();
    }
    render() {
      ...
      return      

那么可以还有Inheritance Inversion做什么呢?

  1. 渲染劫持(Render Highjacking)
  2. 操作 state

1,渲染劫持

之所以被称为渲染劫持是因为 HOC 控制着 WrappedComponent 的渲染输出,可以用它做各种各样的事。

通过渲染劫持,你可以完成:

  1. 在由 render输出的任何 React 元素中读取、添加、编辑、删除 props
  2. 读取和修改由 render 输出的 React 元素树
  3. 有条件地渲染元素树
  4. 把样式包裹进元素树,就行Props Proxy那样包裹其他的元素

注:在 Props Proxy 中不能做到渲染劫持。

虽然通过 WrappedComponent.prototype.render 你可以访问到 render 方法,不过还需要模拟 WrappedComponent 的实例和它的 props,还可能亲自处理组件的生命周期,而不是交给 React。记住,React 在内部处理了组件实例,你处理实例的唯一方法是通过 this 或者 refs。

2,操作 state

HOC 可以读取、编辑和删除 WrappedComponent 实例的 state,如果你需要,你也可以给它添加更多的 state。不过这可能会搞坏 WrappedComponent 的 state,从而造成一些莫名其妙的问题。所以,正确的做法是,要限制 HOC 读取或添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 state 混在一起。

例如:通过访问 WrappedComponent 的 props 和 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>
      )
    }
  }
}      

用 HOC 包裹了一个组件会使它失去原本 WrappedComponent 的名字,可能会影响开发和调试。通常会用 WrappedComponent 的名字加上一些前缀作为 HOC 的名字。

例如:

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`

//或

class HOC extends ... {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`
  ...      

getDisplayName 函数内容:

function getDisplayName(WrappedComponent)
  return      

相关案例

​​react-redux​​

React-Redux 是 Redux 官方的 React 绑定实现。他提供的函数中有一个 connect,处理了监听 store 和后续的处理。是通过 Props Proxy 来实现的。