深入理解 React 高阶组件(Higher Order Component,简称:HOC)

10年服务1亿前端开发工程师

小编推荐:掘金是一个面向程序员的高质量技术社区,从 一线大厂经验分享到前端开发最佳实践,无论是入门还是进阶,来掘金你不会错过前端开发的任何一个技术干货。

摘要

本文针对希望利用 HOC 模式的高级用户。如果对 React 组件模式感到陌生,请先查看相关的介绍(http://www.css88.com/archives/9426 或者 http://www.css88.com/archives/9458)。 如果您是 React 的新手,您应该首先阅读 React 的文档(中文)

高阶组件是一个伟大的模式,很多 React 库已经证明了其非常有价值。 在本文中,我们将详细介绍 HOC 是什么,您可以对它们做些什么,它们的局限性,以及它们的实现方式。

在附录中我们提及了相关的主题,虽然不是 HOC 研究核心,但我认为我们应该涵盖这些主题。

这篇文章尽量做到详尽无遗,所以如果你找到我错过了任何内容,请告诉我,我会做出必要的修改。

本文假定已经掌握了 ES6 相关的知识。

让我们开始吧!

什么是高阶组件?

高阶组件只是一个包裹了另一个组件的 React 组件。

愚人码头注1:确切的说高阶组件是一种 React 组件模式,它是一个 JavaScript 函数,将组件作为参数并返回一个新组件。

愚人码头注2:高阶组件,英文为 Higher Order Component,简称:HOC。

这种模式通常作为一个函数实现,它基本上是一个类工厂(是的,一个类工厂!),它的函数签名可以用类似 haskell 的伪代码表示

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

其中 W(WrappedComponent) 是被包裹的 React.Component,而 E(Enhanced Component,增强组件) 是返回的新的 HOC ,React.Component。

我故意模糊了定义中 “包裹” 的概念,因为它可能意味着两件事之一:

  1. Props Proxy(属性代理): HOC 对传给 WrappedComponent W 的 porps 进行操作,
  2. Inheritance Inversion(反向继承): HOC 扩展了 WrappedComponent W。

我将更详细地探索这两种方式。

我可以用来 HOC 做什么?

在高层次上,HOC 允许你:

  • 代码重用,逻辑和引导抽象
  • 渲染劫持(Render Highjacking)
  • state(状态)抽象和操作
  • props(道具)操作

我们很快会更详细地介绍这些内容,但首先,我们将研究实现 HOC(高阶组件) 的方法,因为这些实现允许并限制了你用 HOC 来做什么。


HOC 工厂的实现

在本节中,我们将研究在 React 中实现 HOC 的两种主要方式:Props Proxy(属性代理)(PP)和 Inheritance Inversion(反向继承)(II) 。 两者都支持不同的方式来操作 WrappedComponent

Props Proxy(属性代理)

Props Proxy(属性代理)(PP) 以下列方式实现:

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

这里的重要部分是 HOC 的 render 方法 返回 WrappedComponent 类型的 React 元素。我们还通过 HOC 接收到的 props(属性),这就是名字 Props Proxy 的由来。

注意:

<WrappedComponent {...this.props}/>
// is equivalent to
React.createElement(WrappedComponent, this.props, null)

它们都创建了一个 React 元素,用于描述 React 在其一致性比较过程中应渲染的内容,
如果您想了解更多有关 React Elements vs Components 的信息,请参阅 Dan Abramov的这篇文章,并查看文档以了解 一致性比较过程(中文)的更多信息。

使用 Props Proxy 可以做些什么?

  • 操作 props(属性)
  • 通过 Refs 访问到组件实例
  • 提取 state(状态)
  • 用其他元素包裹 WrappedComponent

操作 props(属性)

你可以读取、添加、编辑、删除传给 WrappedComponent 的 props(属性)。

在删除或编辑重要的 props(属性) 时要小心,你应该通过命名空间确保高阶组件的 props 不会破坏 WrappedComponent

示例:添加新 props(属性)。 应用程序中当前登录的用户可以在 WrappedComponent 中通过 this.props.user访问。

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

通过 Refs 访问到组件实例

您可以使用 ref(引用) 访问 this ( WrappedComponent 的实例),但是您需要 WrappedComponent 完成正常的初始渲染过程才能计算 ref(引用) ,这意味着您需要从 HOC 的 render 方法中返回 WrappedComponent 元素 ,让 React 执行它的一致性比较过程,然后您将获得 WrappedComponent 实例的引用。

示例:在下面的示例中,我们将探索如何通过 refs 访问实例方法和 WrappedComponent 的实例本身

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}/>
    }
  }
}

渲染 WrappedComponent 时,将执行 ref 回调,然后您将获得对 WrappedComponent 实例的引用。这可以用于读取/添加实例 props(属性) 和调用实例方法。

提取 state(状态)

您可以通过向 WrappedComponent 提供 props(属性) 和回调来提取 state(状态),非常类似于智能(Smart)组件如何处理非智能(Dumb)组件。有关更多信息,请参阅非智能(Dumb)和智能(Smart)组件

示例:在以下提取 state(状态)示例中,我们非常规地提取 name 输入字段的值和 onChange 处理程序。 我说的是非常规,因为这不是很常规,但你必须看到这一点。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }
      
      this.onNameChange = this.onNameChange.bind(this)
    }
    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }
    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange
        }
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

你可以像这样使用它:

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

input 会自动成为 受控input

愚人码头注:这里使用了 JavaScript decorator(装饰器) 语法。

更多关于常规的双向绑定 HOC 请点击这个 链接

用其他元素包裹 WrappedComponent

您可以将 WrappedComponent 与其他组件和元素包装在一起,以用于样式,布局或其他目的。 一些基本用法可以通过常规父组件来完成(参见附录B),但如前所述,但是你可以通过 HOC 获得更多灵活性。

例子:为样式目的而包裹

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

Inheritance Inversion(反向继承)

Inheritance Inversion(反向继承)(II) 通过以下方式实现:

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

如您所见,返回的HOC类( Enhancer继承(extends)WrappedComponent 。 它被称为 Inheritance Inversion(反向继承),因为它不是用 WrappedComponent 来继承某些 Enhancer 类。而是被 Enhancer 被动继承。 通过这种方式,它们之间的关系似乎是 反向(inverse)

反向继承允许 HOC 通过 this 访问 WrappedComponent 实例,这意味着它可以访问 state(状态),props(属性),组件生命周期方法和 render 方法。

我不会详细介绍你可以用生命周期方法来做什么,因为它不是 HOC 的特性,而是 React 的特性。 但请注意,使用 Inheritance Inversion(反向继承),您可以为 WrappedComponent 创建新的生命周期方法。 记得总是这样调用 super.[lifecycleHook] ,这样就不会破坏 WrappedComponent

一致性比较处理(Reconciliation process)

在深入了解之前,让我们总结一些概念。

React 元素描述当 React 执行 一致性比较(reconciliation) 过程时将要渲染的内容。

React 元素有两种类型:StringFunction。 字符串类型 React 元素(STRE)表示 DOM 节点,函数类型 React 元素(FTRE)表示通过继承 React.Component 创建的组件。 有关元素和组件的更多信息,请阅读 此文章

在 React 的 一致性比较(reconciliation) 过程中,FTRE 将被解析为完整的 STRE 树(最终结果将始终是 DOM 元素)。

这非常重要,这意味着 Inheritance Inversion(反向继承) 的高阶组件无法保证解析完整的子树

Inheritance Inversion(反向继承) 的高阶组件无法保证解析完整的子树。

在学习 Render Highjacking(渲染劫持)时,将被证明这点非常重要。

你可以用 Inheritance Inversion(反向继承) 来做什么?

  • 渲染劫持(Render Highjacking)
  • 操作 state(状态)

渲染劫持(Render Highjacking)

它被称为 渲染劫持(Render Highjacking),因为 HOC 控制了 WrappedComponent 的渲染输出,并且可以用它做各种各样的事情。

在渲染劫持中,您可以:state(状态),props(属性)

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

注:render 是指 WrappedComponent.render 方法

您无法编辑或创建 WrappedComponent 实例的 props(属性),因为 React 组件无法编辑它接收到的 props(属性),但是您可以更改从 render 方法输出的元素的 props(属性)。

正如我们之前研究的那样,Inheritance Inversion(反向继承) 类型的 HOC 无法保证解析完整的子树,这意味着 渲染劫持(Render Highjacking) 技术有一些限制。经验法则是,使用 渲染劫持(Render Highjacking),你可以完全操作 WrappedComponent 的 render 方法返回的元素树。如果该元素树包含函数式 React Component,那么您将无法操作该组件的子元素。它们被 React 的一致性比较过程推迟,直到它实际渲染到屏幕上。)

示例1:条件渲染。除非 this.props.loggedIn 不为 true ,否则此 HOC 将准确渲染 WrappedComponent 将渲染的内容。(假设 HOC 将收到 loggedIn props(属性))

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

示例2:修改由 render 方法输出的 React 组件树。

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      const elementsTree = super.render()
      let newProps = {};
      if (elementsTree && elementsTree.type === 'input') {
        newProps = {value: 'may the force be with you'}
      }
      const props = Object.assign({}, elementsTree.props, newProps)
      const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children)
      return newElementsTree
    }
  }
}

在这个例子中,如果 WrappedComponent 的渲染输出有一个 input 作为它的顶级元素,那么我们将 value 设为 “may the force be with you”。

你可以在这里做各种各样的事情,你可以遍历整个元素树并改变树中任何元素的 props(属性)。 这也正是样式处理库 Radium 所用的方法(更多关于 Radium 的案例研究)。

注:在 Props Proxy(属性代理) 类型的高阶函数中做不到渲染劫持。

虽然可以通过 WrappedComponent.prototype.render 访问 render 方法,但是您需要模拟 WrappedComponent 实例及其 props(属性),并且可能需要自己处理组件生命周期,而不是依赖 React 执行它。 在我的实验中不值得这么做,如果你想做渲染劫持(Render Highjacking),你应该使用 Inheritance Inversion(反向继承) 而不是 Props Proxy(属性代理)。 请记住,React 在内部处理组件实例,而处理实例的唯一方法是通过 thisrefs

操作 state(状态)

HOC可以读取,编辑和删除 WrappedComponent 实例的状态,如果需要,还可以添加更多的 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 的实例 props(属性) 和 state(状态) 。Ryan Florence 和 Michael Jackson 教我了 JSON.stringify 技巧。您可以在 此处 查看调试器的完整实现。


命名

使用 HOC 包裹组件时,会丢失原始 WrappedComponent 的名称,这可能会在开发和调试时影响到您。

人们通常做的是通过获取 WrappedComponent 的名称并预先添加某些内容来自定义 HOC 的名称。 以下内容摘自 React-Redux 。

用 HOC 包裹了一个组件会使它失去原本 WrappedComponent 的名字,可能会影响开发和调试。

通常会用 WrappedComponent 的名字加上一些 前缀作为 HOC 的名字。下面的代码来自 React-Redux:

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

getDisplayName 函数定义如下:

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName ||
         WrappedComponent.name ||
         'Component'
}

实际上你不需要自己重写它,因为recompose 库已经提供了这个函数。


案例分析

React-Redux

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

如果您曾经使用过纯 Flux ,那么您知道连接到一个或多个 store 的任何React组件都需要大量的引导来添加和删除 store 监听器,并选择所需 state 的部分。 所以 React-Redux 的实现非常好,因为它抽象了所有这些引导程序。 基本上,你不需要自己写了!

Radium

Radium是一个通过在内联样式中启用 CSS 伪选择器来增强内联样式功能的库。为什么内联样式对你有好处是另一个讨论的主题,但很多人都开始这样做,像 Radium 这样的库真的简化了这个过程。如果您想了解更多关于内联样式的信息,请参阅 Vjeux 的这个 ppt

那么,Radium 如何启用内嵌CSS伪选择器的呢,比如 hover ?它实现了一个 Inheritance Inversion(反向继承) 模式,使用 Render Highjacking(渲染劫持) 来注入适当的事件监听器(新的 props )来模拟CSS伪选择器,如 hover 。事件监听器作为 React 元素的 props(属性) 的处理程序注入。这需要 Radium 读取 WrappedComponent 的 render 方法输出的所有元素树,每当找到一个新的带有 style 属性的组件时,它都会在 props 上添加一个事件监听器。简单地说,Radium 修改了元素树的 props(属性) (实际上 Radium 的实现会更复杂些,你理解意思就行)。

Radium 公开了一个非常简单的 API 。 想象一下它在没有用户注意的情况下执行的所有工作,令人印象深刻。 这让人们看到了 HOC 的能力。


附录 A: HOC 和 参数

以下内容是可选的,您可以跳过它。

有时在 HOC 上使用参数很有用。 这在上面的所有示例中都是隐含的,对于中级 Javascript 开发人员来说应该是非常自然的,但仅仅是为了使帖子详尽无遗,让我们快速地浏览一下。

示例:带有 HOC 参数的普通 Props Proxy 模式。 关键在于 HOCFactoryFactory 函数。

function HOCFactoryFactory(...params){
  // do something with params
  return function HOCFactory(WrappedComponent) {
    return class HOC extends React.Component {
      render() {
        return <WrappedComponent {...this.props}/>
      }
    }
  }
}

你可以这样使用:

HOCFactoryFactory(params)(WrappedComponent)
// 或
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}

附录 B: 与父组件的不同

以下内容是可选的,您可以跳过它。

父组件只是具有一些子组件的 React 组件。 React 具有用于访问和操作子级组件的API。

示例:父组件访问其子组件。

class Parent extends React.Component {
    render() {
      return (
        <div>
          {this.props.children}
        </div>
      )
    }
}

render((
  <Parent>
    {children}
  </Parent>
  ), mountNode)

现在我们将回顾一下与 HOC 相比,父组件可以做什么,不可以做什么以及一些重要的细节:

  • 渲染劫持(如反向继承中所示)
  • 操纵内部 props(属性)(如反向继承中所示)
  • 提取 state(状态)。但有它的缺点。除非为其明确创建钩子,否则您将无法从外部访问父组件的状态。这使其有用性受到限制。
  • 用新的 React 元素包裹。这可能父组件感觉比 HOC 更好用的唯一用例。 HOC 也可以做到这一点。
  • 操作子组件会有一些问题。例如,如果子组件没有单个根元素(多个第一级子元素),那么您需要添加一个额外的元素来包裹所有子元素,这可能对您的标记有点麻烦。在 HOC 中单一的根节点会由 React/JSX语法来确保。
  • 父组件可以在元素树中自由使用,它们不像 HOC 那样限制为每个组件创建一个类。

一般来说,如果你可以使用父组件,你应该这样做,因为它比 HOC 更少hacky,但正如上面的列表所述,它们不如 HOC 灵活。

结束语

我希望在阅读这篇文章后你会对 React HOC 有更多的了解。它们具有很强的能力,并且在不同的库中已被证明相当不错。

React 带来了很多创新,人们使用 Radium,React-Redux,React-Router 等运行项目就是很好的证明。

跳转到 这个仓库 来玩我玩过的代码,以试验这篇文章中解释的一些模式。

更多关于 React 高阶组件请参阅React 官方文档(中文)

原文链接:https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e


如果你觉得本文对你有帮助,那就请分享给更多的朋友
关注「前端干货精选」加星星,每天都能获取前端干货
赞(0) 打赏
未经允许不得转载:WEB前端开发 » 深入理解 React 高阶组件(Higher Order Component,简称:HOC)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

前端开发相关广告投放 更专业 更精准

联系我们

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏