您现在的位置:首页 >> 前端 >> 内容

React高阶组件(Higher-OrderComponents)

时间:2017/8/4 17:14:34 点击:

  核心提示:使用 react已经有不短的时间了,不过一直没深入研究过,最近看到关于 react高阶组件的一篇文章,看了之后觉得是个不可多得的 zhuangbility的利器,自然不可轻易错过,遂深入了解了一番。概...

使用 react已经有不短的时间了,不过一直没深入研究过,最近看到关于 react高阶组件的一篇文章,看了之后觉得是个不可多得的 zhuangbility的利器,自然不可轻易错过,遂深入了解了一番。


概述

高阶组件的定义

React 官网上对高阶组件的定义:

高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式。
具体地说,高阶组件就是一个接收一个组件并返回另外一个新组件的函数。
相比于普通组件将 props 转化成界面UI,高阶组件将一个普通组件转化为另外一个组件。

功能

可以利用高阶组件来做的事情:

代码复用,逻辑抽象,抽离底层准备(bootstrap)代码 Props 更改 State 抽象和更改 渲染劫持

用法示例

基本用法

一个最简单的高阶组件(HOC) 示例如下:
// HOCComponent.js

import React from 'react'

export default PackagedComponent =>
  class HOC extends React.Component {
    render() {
      return (
        

Title

) } }

此文件导出了一个函数,此函数返回经过一个经过处理的组件,它接受一个参数 PackagedComponent,此参数就是将要被 HOC包装的普通组件,接受一个普通组件,返回另外一个新的组件,很符合高阶组件的定义。

此高阶组件的简单使用如下:
// main.js
import React from 'react'
// (1)
import HOCComponent from './HOCComponent'

// (2)
@HOCComponent 
class Main extends React.Component {
  render() {
    return(
      

main content

) } } // (2) // 也可以将上面的 @HOCComponent换成下面这句 // const MainComponent = HOCComponent(Main) export default MainComponent

想要使用高阶组件,首先(1)将高阶组件导入,然后(2)使用此组件包装需要被包装的普通组件 Main,这里的@符号是 ES7中的decorator,写过Java或者其他静态语言的同学应该并不陌生,这实际上就是一个语法糖,可以使用 react-decorators 进行转换, 在这里相当于下面这句代码:

const MainComponent = HOCComponent(Main)

@HOCComponent完全可以换成上面那句,只不过需要注意的是,类不具有提升的能力,所以若是觉得上面那句顺眼换一下,那么在换过之后,还要将这一句的位置移到类Main定义的后面。

最后,导出的是被高阶组件处理过的组件 MainComponent

这样,就完成了一个普通组件的包装,可以在页面上将被包装过的组件显示出来了:
import React from 'react'
import { render } from 'react-dom'

// 导入组件
import MainComponent from './main'

render(
  ,
  document.getElementById('root')
)

页面显示如下:
React高阶组件(Higher-OrderComponents)

可以使用 React Developer Tools查看页面结构:
React高阶组件(Higher-OrderComponents)

可以看出,组件Main的外面包装了一层 HOC,有点类似于父组件和子组件,但很显然高阶组件并不等于父组件。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPsHtzeLQ6NKq16LS4rXE0ru146OsIEhPQ9XiuPa4373X1+m8/qOsztLDx7/JxNy74dPDtb2yu9a50ru0zqOsuabE3Ly8yvXJz8O7yrLDtLnYz7WjrLWryseyu8D709q198rUo6zOqsHLv+zL2bXYx/i31rP2xLO49sbVzajX6bz+tcTL+cr0tcRIT0O1vbXXysfExNK7uPajrM7Sw8e/ydLUuPjV4tCpIEhPQ7340NDD/MP7o7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:sql;"> // 获取传入的被包装的组件名称,以便为 HOC 进行命名 let getDisplayName = component => { return component.displayName || component.name || 'Component' } export default PackagedComponent => class HOC extends React.Component { // 这里的 displayName就指的是 HOC的显示名称,我们将它重新定义了一遍 // static被 stage-0 stage-1 和 stage-2所支持 static displayName = `HOC(${getDisplayName(PackagedComponent)})` render() { return (

Title

) } }

现在的页面结构:
React高阶组件(Higher-OrderComponents)

可以看到,原先的HOC已经变成了 HOC(Main)了,这么做主要是利于我们的调试开发。

这里的HOC,可以看做是一个简单的为普通组件增加Title的高阶组件,但是很明显并不是所有的页面都只使用同一个标题,标题必须要可定制化才符合实际情况。

想做到这一点也很简单,那就是再为HOC组件的高阶函数增加一个 title参数,另外考虑到 柯里化 Curry函数和函数式编程,我们修改后的 HOC代码如下:

// HOCComponent.js

// 增加了一个函数,这个函数存在一个参数,此参数就是要传入的`title`
export default PackagedComponent => componentTitle =>
  class HOC extends React.Component {
    static displayName = `HOC(${getDisplayName(PackagedComponent)})`
    render() {
      return (
        

{ componentTitle ? componentTitle : 'Title' }

) } }

使用方式如下:

// main.js

// ...省略代码
const MainComponent = HOCComponent(Main)('首页')
export default MainComponent

然后在页面上就可以看到效果了:

React高阶组件(Higher-OrderComponents)


属性代理

因为 HOC是包裹在普通组件外面的一层高阶函数,所以任何要传入普通组件内的props 或者 state 首先都要经过 HOC,这就让 HOC拥有了提前对这些属性进行修改的能力。

更改 Props

对 Props 的更改操作包括 增、删、改、查,在修改和删除 Props的时候需要注意,除非特殊要求,否则最好不要影响到原本传递给普通组件的 Props

class HOC extends React.Component {
    static displayName = `HOC(${getDisplayName(PackagedComponent)})`
    render() {
      // 向普通组件增加了一个新的 `Props`
      const newProps = {
        summary: '这是内容'
      }
      return (
        

{ componentTitle ? componentTitle : 'Title' }

) } }

通过 refs 获取组件实例

普通组件如果带有一个 ref属性,当其通过 HOC的处理后,已经无法通过类似 this.refs.component的形式获取到这个普通组件了,只会得到一个被处理之后的组件,想要仍然获得原先的普通组件,需要对 ref进行处理,一种处理方法类似于 react-readux 中的 connect方法,如下:

// HOCComponnet.js
...
export default PackagedComponent => componentTitle =>
  class HOC extends React.Component {
    static displayName = `HOC(${getDisplayName(PackagedComponent)})`
    // 回调方法,当被包装组件渲染完毕后,调用被包装组件的 changeColor 方法
    propc(wrapperComponentInstance) {
      wrapperComponentInstance.changeColor()
    }
    render() {
      // 改变 props,使用 ref 获取被包装组件的示例,以调用其中的方法
      const props = Object.assign({}, this.props, {ref: this.propc.bind(this)})
      return (
        

{ componentTitle ? componentTitle : 'Title' }

) } }

使用:

// main.js
...
class Main extends React.Component {
  render() {
    return(
      

main content

{ this.props.summary }
) } // main.js 中的changeColor 方法 changeColor() { console.log(666); document.querySelector('p').style.color = 'greenyellow' } } ...

基于以上,我想到了一种可能,既然高阶组件能够代理到 普通组件的Props 和 state等属性,那么在使用诸如 redux等库的时候,是不是可以让高阶组件来承接这些由 redux传递到全局的属性,然后再用高阶组件包装普通组件,将获得的属性传递给普通组件,这样普通组件就能获取到 这些全局属性了。

相比于使用 redux一个个地初始化所有需要使用到全局属性的组件,使用高阶组件作为载体,虽然结构上多了一层,但是操作上明显方便简化了许多,我还没有验证过,有机会的话再试试。


反向继承(Inheritance Inversion)

说的通俗点,相比于前面使用 HOC包装在 普通组件外面的情况,反向继承就是让HOC继承普通组件、打入普通组件的内部,这种情况下,普通组件变成了基类,而HOC变成了子类,子类能够获得父类所有公开的方法和字段。

反向继承高阶组件的功能:

能够对普通组件生命周期内的所有钩子函数进行覆写 对普通组件的 state进行增删改查的操作。
// HOCInheritance.js

let getDisplayName = (component)=> {
  return component.displayName || component.name || 'Component'
}

// (1)
export default WrapperComponent =>
class Inheritance extends WrapperComponent {
  static displayName = `Inheritance(${getDisplayName(WrapperComponent)})`
  // (2)
  componentWillMount() {
    this.state.name = 'zhangsan'
    this.state.age = 18
  }
  render() {
    // (4)
    return super.render()
  }
  componentDidMount() {
    // 5
    super.componentDidMount()
    // 6
    document.querySelector('h1').style.color = 'indianred'
  }
}

上述代码中,让 Inheritance 继承 WrapperComponent (1),
并且覆写了WrapperComponent 中的 componentWillMount函数(2),
在这个方法中对 WrapperComponent 的 state进行操作(3),
在 render方法中,为了防止破坏WrapperComponent原有的 render()方法,使用 super将 WrapperComponent 中原有的 render方法实现了一次(4),
在 componentDidMount同样是先将 WrapperComponent 中的 componentDidMount方法实现了一次(5),
并且在原有的基础上,又进行了一些操作(6)

需要注意的是,super并不是必须使用,这取决于你是否需要实现普通组件中原有的对应函数,一般来说都是需要的,类似于 mixin,至于到底是原有钩子函数中的代码先执行,还是 HOC中另加的代码先执行,则取决于 super的位置,如果super在新增代码之上,则原有代码先执行,反之亦然。

另外,如果普通组件并没有显性实现某个钩子函数,然后在HOC中又添加了这个钩子函数,则 super不可用,因为并没有什么可以 super的,否则将报错。

使用:

// main2.js

import React from 'react'
import Inheritance from './HOCInheritance'

class Main2 extends React.Component {
  state = {
    name: 'wanger'
  }
  render() {
    return (
      

summary of

my name is {this.state.name}, I'm {this.state.age}

) } componentDidMount() { document.querySelector('h1').innerHTML += this.state.name } } const InheritanceInstace = Inheritance(Main2) export default InheritanceInstace

页面效果:
React高阶组件(Higher-OrderComponents)

可以看出,HOC为原有组件添加了 componentWillMount函数,在其中覆盖了 Main2中 state的 ‘name’属性,并且其上添加了一个age属性

HOC还将 Main的 componentDidMount方法实现了一次,并且在此基础上,实现了自己的 componentDidMount方法。


注意事项

react官网 上还给出了几条关于使用 HOC 时的注意事项。

不要在render函数中使用高阶组件

例如,以下就是错误示范:

// 这是个 render 方法
render() {
  // 在 render 方法中使用了 HOC
  // 每一次render函数调用都会创建一个新的EnhancedComponent实例
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 每一次都会使子对象树完全被卸载或移除
  return ;
}
静态方法必须复制

HOC 虽然可以自动获得 普通组件的 props和 state等属性,但静态方法必须要手动挂载。

// 定义静态方法
WrappedComponent.staticMethod = function() {/*...*/}
// 使用高阶组件
const EnhancedComponent = enhance(WrappedComponent);

// 增强型组件没有静态方法
typeof EnhancedComponent.staticMethod === 'undefined' // true

为了解决这个问题,在返回之前,可以向容器组件中复制原有的静态方法:

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必须得知道要拷贝的方法
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

或者使用 hoist-non-react-statics来自动复制这些静态方法

Refs不会被传递
对于 react组件来说,ref其实不是一个属性,就像key一样,尽管向其他props一样传递到了组件中,但实际上在组件内时获取不到的,它是由React特殊处理的。如果你给高阶组件产生的组件的元素添加 ref,ref引用的是外层的容器组件的实例,而不是被包裹的组件。

想要解决这个问题,首先是尽量避免使用 ref,如果避免不了,那么可以参照本文上面提到过的方法。

Tags:RE EA AC CT 
作者:网络 来源:Quiet-Nigh