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 <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

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 <WrappedComponent {...props}/>
    }
  }
}

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 super.render();
    }
  }
}

那么可以还有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 WrappedComponent.displayName ||
         WrappedComponent.name ||
         ‘Component’
}

相关案例

react-redux

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

radium Radium 通过在内联样式中使用CSS 伪类增强了内联样式的能力。 话说回来,Radium 是怎样做到内联 CSS 伪类的,比如 hover?它用 Inheritance Inversion 模式做到了渲染劫持,插入对应的事件监听器来模拟 CSS 伪类,比如 hover。事件监听器插入到了 React 组件的 props 里。Radium 需要读取 WrappedComponent 的 render 方法输出的所有组件树,每当它发现一个新的带有 style 属性的组件时,在 props 上添加一个事件监听器。通俗的讲:Radium 修改了组件树的 props,从而实现界面渲染改变。

附:精读高阶组件

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏谦谦君子修罗刀

RN项目第二节 -- 首页实现

37630
来自专栏AhDung

【C#】使用IExtenderProvider为控件添加扩展属性,像ToolTip那样

- 文中的“控件”一词是广义上的说法,泛指包括ToolStripItem、MenuItem在内单个界面元素,并不特指继承自Control类的狭义控件

11320
来自专栏nummy

使用jsTree树形控件【3】HTML结构

jsTree可以将无序列表转换成树形结构,最简单的HTML结构就是使用<ul>以及<li>,而且最好外面还嵌套了一个<div> $('#html1').jst...

21720
来自专栏flutter开发者

[Flutter Widget]ExpansionPanelList

在前面的文章中我们介绍了可以展开的带标题控件ExpansionTile的用法,在文章的最后还是按照惯例给大家留下了一个问题。

1.6K30
来自专栏偏前端工程师的驿站

WebComponent魔法堂:深究Custom Element 之 标准构建

前言  通过《WebComponent魔法堂:深究Custom Element 之 面向痛点编程》,我们明白到其实Custom Element并不是什么新东西,...

212100
来自专栏贾鹏辉的技术专栏@CrazyCodeBoy

React Native之React速学教程(下)

React Native之React速学教程(下) 本文出自《React Native学习笔记》系列文章。 React Native是基于React的,在开发R...

32650
来自专栏Android干货

安卓开发中strings.xml的使用

21750
来自专栏我和未来有约会

用后台代码创建Storyboard

string storyboardName = "MyStoryBoard"; string myXamlElement = "MyXamlElement"...

21990
来自专栏腾讯社交用户体验设计

面向未来的 CSS Variable

9520
来自专栏非著名程序员

Android 属性动画详解,属性动画基本用法

Hello,大家好,今天又来装逼了,装逼也上瘾啊,最近公司不是特别忙,我想这也就是我出来装逼的最好时机吧!额,,哈哈,进入正题。如有疑问欢迎留言,如有谬误欢迎批...

25150

扫码关注云+社区

领取腾讯云代金券