深入 React 高阶组件

时间: 2018-06-12阅读: 1134标签: react


概要

本文面向想要探索 HOC 模式的进阶用户,如果你是 react 的初学者则应该从官方文档开始。高阶组件(Higher Order Components)是一种很棒的模式,已被很多 react 库证实是非常有价值的。在本文中,我们首先回顾一下 HOC 是什么、有什么用、有何局限,以及是如何实现它的。

在附录中,检视了相关的话题,这些话题并非 HOC 的核心,但我认为应该提及。

本文旨在尽量详细的论述,以便于读者查阅;并假定你已经知晓 ES6。

走你!


高阶组件是什么?

高阶组件就是包裹了其他 React Component 的组件

通常,这个模式被实现为一个函数,基本算是个类工厂方法(yes, a class factory!),其函数签名用 haskell 风格的伪代码写出来就是这样的:

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

W (WrappedComponent) 是被包裹的 React.Component;而函数返回的 E (Enhanced Component) 则是新得到的 HOC,也是个 React.Component。

定义中的“包裹”是一种有意的模糊,意味着两件事情:

  • 属性代理:由 HOC 操纵那些被传递给被包裹组件 W 的 props

  • 继承反转:HOC 继承被包裹组件 W

后面会详述这两种模式的。


HOC 能做什么?

在大的维度上 HOC 能用于:

  • 代码重用和逻辑抽象

  • render 劫持

  • state 抽象和操纵

  • 操纵属性(props)

后面将会看到这些类目的细节,但首先来学习一下实现 HOC 的方式,因为实现方式决定了 HOC 实际能做的事情。


HOC 工厂实现

属性代理(PP)和继承反转(II)。两者皆提供了不同的途径以操纵被包裹的组件。


属性代理

属性代理(Props Proxy)可以用以下方式简单的实现:

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

此处关键的部分在于 HOC 的 render() 方法返回了一个被包裹组件的 React Element。同时,将 HOC 接受到的属性传递给了被包裹的组件,因此称为“属性代理”

注意:

<WrappedComponent {...this.props}/>
// 等价于
React.createElement(WrappedComponent, this.props, null)

两者都会创建一个 React Element,用于描述 React 在其一致性比较过程中应该渲染什么。

了解更多:

关于 React Elment vs Components 的内容可以查看
https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html

一致性比较过程
http://www.css88.com/react/docs/reconciliation.html


可以用属性代理做些什么?

  • 操纵属性

  • 通过 refs 访问实例

  • 抽象 state

  • 包裹组件


操纵属性

可以对传递给被包裹组件的属性进行增删查改。但删除或编辑重要属性时要谨慎,应合理设置 HOC 的命名空间以免影响被包裹组件。

例子:增加新属性。应用中通过 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(被包裹组件的实例),但这需要 ref 所引用的被包裹组件运行一次完整的初始化 render 过程,这就意味着要从 HOC 的 render 方法中返回被包裹组件的元素,并让 React 完成其一致性比较过程,而 ref 能引用该组件的实例就好了。

例子:下例中展示了如何通过 refs 访问到被包裹组件的实例方法和实例本身

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 回调就将执行,由此就能获得其实例的引用。这可以用于读取、增加实例属性,或调用实例方法。


抽象 state

通过提供给被包裹组件的属性和回调,可以抽象 state,这非常类似于 smart 组件是如何处理 dumb 组件的。

关于上述两种组件可以参阅:
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

例子:在下面这个抽象 state 的例子里我们简单的将 value 和 onChange 处理函数从 name 输入框中抽象出来。之所以说“简单”是因为这非常普遍,但你必须明白这一点。

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

于是这个输入框就自动成为了一个受控组件。

关于受控组件:
https://mp.weixin.qq.com/s/I3aPxyZA_iArUDmsXtXGcw


包裹组件

可以利用组件的包裹,实现样式定义、布局或其他目标。一些基础用法可以由普通的父组件完成(参阅附录B),但如前所述,用 HOC 可以更加灵活。

例子:为定义样式而实现的包裹

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()
    }
  }
}

如你所见,被返回的 HOC 类(强化过的类)继承了被包裹的组件。之所以被称为“继承反转”是因为,被包裹组件并不去继承强化类,而是被动的让强化类继承。通过这种方式,两个类的关系看起来反转了。

继承反转使得 HOC 可以用 this 访问被包裹组件的实例,这意味着可以访问 state、props、组件生命周期钩子,以及 render 方法

这里并不深入探讨可以在生命周期钩子中实现的细节,因为那属于 React 的范畴。但要知道通过继承反转可以为被包裹组件创建新的生命周期钩子;并记住总是应该调用 super.[lifecycleHook] 以确保不会破坏被包裹的组件。


一致性比较过程

在深入之前我们大概说一下这些理论。

一致性比较
https://facebook.github.io/react/docs/reconciliation.html

React Elements 描述了 React 运行其一致性比较过程时,什么会被渲染。

React Elements 可以是两种类型:字符串和函数。字符串类型的 React Elements(STRE)代表 DOM 节点,函数类型的 React Elements(FTRE)代表继承自 React.Component 的组件。

React 元素和组件
https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html

在 React 的一致性比较过程(最终结果是 DOM 元素)中,FTRE 会被处理成一棵完整的 STRE 树。

之所以很重要,就在于这意味着继承反转高阶组件并不保证处理完整的子树

后面学习到 render 劫持的时候将会证明其重要性。


可以用继承反转做些什么?

  • render 劫持

  • 操纵 state


render 劫持

称之为“render 劫持”是因为 HOC 控制了被包裹组件的 render 输出,并能对其做任何事情。

在 render 劫持中可以:

  • 在任何 render 输出的 React Elements 上增删查改 props

  • 读取并修改 render 输出的 React Elements 树

  • 条件性显示元素树

  • 出于定制样式的目的包裹元素树(正如属性代理中展示的)

*用 render 引用被包裹组件的 render 方法

不能对被包裹组件的实例编辑或创建属性,因为一个 React Component 无法编辑其收到的 props,但可以改变被 render 方法输出的元素的属性。

就如我们之前学到的,继承反转 HOC 不保证处理完整的子树,这意味着 render 劫持技术有一些限制。经验法则是,借助于 render 劫持,可以不多不少的操作被包裹组件的 render 方法输出的元素树。如果那个元素数包含了一个函数类型的 React Component,那就无法操作其子组件(被 React 的一致性比较过程延迟到真正渲染到屏幕上时)。

例子1:条件性渲染

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

例子2:修改 render 输出的元素树

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

本例中,如果由 render 输出的被包裹组件有一个 input 顶级元素,就改变其 value。

可以在这里做任何事情,可以遍历整个元素树并改变其中的任何一个元素属性。

注意:不能通过属性代理劫持 render

虽然通过 WrappedComponent.prototype.render 访问 render 方法是可能的,但这样一来你就要模拟被包裹组件的实例及其属性,并自己处理组件生命周期而非依靠 React 去解决。以我的经验来说这是得不偿失的,如果要劫持 render 应该用继承反转而非属性代理。要记住 React 内在地处置组件实例,而你只能通过 this 或 refs 来处理实例。


操纵 state

HOC 可以读取、编辑和删除被包裹组件实例的 state,也可以按需增加更多的 state。要谨记如果把 state 搞乱会很糟糕。大部分 HOC 应该限制读取或增加 state,而后者(译注:增加 state)应该使用命名空间以免和被包裹组件的 state 搞混。

例子:对访问被包裹组件的 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 将被包裹组件嵌入其他元素中,并显示了其 props 和 state。


命名

使用 HOC 时,就失去了被包裹组件原有的名字,可能会影响开发和调试。

人们通常的做法就是用原有名字加上些什么来命名 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 库(https://github.com/acdlite/recompose)已经提供了。


附录 A:HOC 和参数

以下为可以跳过的选读内容

在 HOC 中可以善用参数。这本来已经在上面所有例子中隐含的出现过,并且对于中级 JS 开发者也已经稀松平常了,但是本着知无不言的原则,还是快速过一遍吧。

例子:结合属性代理和 HOC 参数,需要关注的是 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)
//or
@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,来细数一下父组件能做和不能做的:

  • render 劫持(在继承反转中看见过)

  • 控制内部 props(同样在继承反转中看见过)

  • 抽象 state,但存在缺点。将无法在外部访问父元素的 state,除非特意为止创建钩子。这限制了其实用性

  • 包裹新的 React Elements。这可能是父组件唯一强于 HOC 的用例,虽然 HOC 也能做到

  • 操纵子组件有一些陷阱。比如说如果 children 的根一级并不只有单一的子组件(多于一个的第一级子组件),你就得添加一个额外的元素来收纳所有子元素,这会让你的代码有些冗繁。在 HOC 中一个单一的顶级子组件是被 React/JSX 的约束所保证的。

通常,父组件的做法没有 HOC 那么 hacky,但上述列表是其相比于 HOC 的不灵活之处。


结语

希望阅读本文后你能对 React HOC 多一些了解。在不同的库中,HOC 都被证明是很有价值并非常好用的。React 带来了很多创新,人们广泛应用着 Radium、React-Redux、React-Router 等等,也很好的印证了这一点。


原文来源: https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e  
翻译来源:https://mp.weixin.qq.com/s/dtlrOGTjoneOIiM5kB3XvQ


站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/874

关闭

21 项优化 React App 性能的技术

在 React 内部,React 会使用几项巧妙的小技术,来优化计算更新 UI 时,所需要的最少的更新 DOM 的操作。在大多数情况下,即使你没有针对性能进行专项优化,React 依然很快,但是仍有一些方法可以加速 React 应用程序

如何写出漂亮的 React 组件

在Walmart Labs的产品开发中,我们进行了大量的Code Review工作,这也保证了我有机会从很多优秀的工程师的代码中学习他们的代码风格与样式。在这篇博文里我会分享出我最欣赏的五种组件模式与代码片。不过我首先还是要谈谈为什么我们需要执着于提高代码的阅读体验

深入理解 React useLayoutEffect 和 useEffect 的执行时机

我们先看下 React 官方文档对这两个 hook 的介绍,建立个整体认识,该 Hook 接收一个包含命令式、且可能有副作用代码的函数。在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器

Context - React跨组件访问数据的利器

Context提供了一种跨组件访问数据的方法。它无需在组件树间逐层传递属性,也可以方便的访问其他组件的数。在经典的React应用中,数据是父组件通过props向子组件传递的

React 服务端渲染完美的解决方案

使用 React 构建客户端应用程序,默认情况下,可以在浏览器中输出 React 组件,进行生成 DOM 和操作 DOM。React 也可以在服务端通过 Node.js 转换成 HTML,直接在浏览器端“呈现”处理好的 HTML 字符串,这个过程可以被认为 “同构”

为什么说React 16是开发者的福音?

就像人们对更新移动应用程序和操作系统感到兴奋一样,开发人员也应该对更新框架感到兴奋。不同框架的新版本具有新特性和开箱即用的技巧。下面是将现有应用程序从 React 15 迁移到 React 16 时应该考虑的一些好特性。

关于创建React App的8个有趣事实

你所知道的一些事情,你可能不知道的一些事情Create React App是搭建React项目的快速方法。这样我们可以重点放在代码上,而不是构建工具上。

grpc-web与react的集成

使用create-react-app脚手架生成react相关部分,脚手架内部会通过node自动起一个客户端,然后和普通的ajax请求一样,和远端服务器进行通信,只不过这里采用支持rpc通信的grpc-web来发起请求,远端采用docker容器的node服务器,node服务器端使用envoy作为代理

react生命周期详解_深入理解React生命周期

React主要思想是通过构建可复用组件来构建用户界面,每个组件都有自己的生命周期,它规定了组件的状态和方法需要在哪个阶段改变和执行。所谓组件就是有限状态机,,表示有限个状态以及在这些状态之间的转移和动作行为的模型。

react-navigation 监听顶部导航栏点击/滑动状态

使用createMaterialTopTabNavigator创建顶部导航栏,希望实现切换到指定的Tab再获取数据,查看官方文档只能找到tabBarOnPress 方法监听点击回调,但是无法监听滑动切换

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!