React 高阶组件浅析

背景

高阶组件的这种写法的诞生来自于社区的实践,目的是解决一些交叉问题(Cross-Cutting Concerns)。而最先时候 React 官方给出的解决方案是使用 mixin 。而 React 也在官网中写道:javascript

We previously recommended mixins as a way to handle cross-cutting concerns. We've since realized that mixins create more trouble than they are worth.css

官方明显也意识到了使用mixins技术来解决此类问题所带来的困扰远高于其自己的价值。更多资料能够查阅官方的说明。html

高阶函数的定义

说到高阶组件,就不得不先简单的介绍一下高阶函数。下面展现一个最简单的高阶函数前端

const add = (x,y,f) => f(x)+f(y)

当咱们调用add(-5, 6, Math.abs)时,参数 x,y 和f 分别接收 -5,6 和 Math.abs,根据函数定义,咱们能够推导计算过程为:java

x ==> -5
y ==> 6
f ==> abs
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11

用代码验证一下:react

add(-5, 6, Math.abs); //11

高阶在维基百科的定义以下git

高阶函数是至少知足下列一个条件的函数:github

  • 接受一个或多个函数做为输入app

  • 输出一个函数dom

高阶组件的定义

那么,什么是高阶组件呢?类比高阶函数的定义,高阶组件就是接受一个组件做为参数并返回一个新组件的函数。这里须要注意高阶组件是一个函数,并非组件,这一点必定要注意。
同时这里强调一点高阶组件自己并非 React API。它只是一种模式,这种模式是由 React 自身的组合性质必然产生的。
更加通俗的讲,高阶组件经过包裹(wrapped)被传入的React组件,通过一系列处理,最终返回一个相对加强(enhanced)的 React 组件,供其余组件调用。

<!-- more -->

一个简单的高阶组件

下面咱们来实现一个简单的高阶组件

export default WrappedComponent => class HOC extends Component {
  render() {
    return (
      <fieldset>
        <legend>默认标题</legend>
        <WrappedComponent {...this.props} />
      </fieldset>
    );
  }
};

在其余组件中,咱们引用这个高阶组件来强化它

export default class Demo extends Component {
  render() {
    return (
      <div>
        我是一个普通组件
      </div>
    );
  }
}

const WithHeaderDemo = withHeader(Demo);

下面咱们来看一下React DOM Tree,调用了高阶组件以后,发生了什么:
图片

能够看到,DemoHOC 包裹(wrapped)了以后添加了一个标题默认标题。可是一样会发现,若是调用了多个 HOC 以后,咱们会看到不少的HOC,因此应
该作一些优化,也就是在高阶组件包裹(wrapped)之后,应该保留原有的名称。

咱们改写一下上述的高阶组件代码,增长一个 getDisplayName 函数,以后为Demo 添加一个静态属性 displayName

const getDisplayName = component => component.displayName || component.name || 'Component';

export default WrappedComponent => class HOC extends Component {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`;

  render() {
    return (
      <fieldset>
        <legend>默认标题</legend>
        <WrappedComponent {...this.props} />
      </fieldset>
    );
  }
};

再次观察React DOM Tree

图片

能够看到,该组件本来的名称已经显示在React DOM Tree上了。
这个HOC 的功能是为原有的组件添加一个标题,也就是说全部须要添加标题的组件均可以经过调用此 HOC 进行包裹(wrapped) 后实现此功能。

为高阶组件传参

如今,咱们的 HOC 已经能够为其余任意组件提供标题了,可是咱们还但愿能够修改标题中的字段。因为咱们的高阶组件是一个函数,因此能够为其添加一个参数title。下面咱们对HOC进行改写:

export default (WrappedComponent, title = '默认标题') => class HOC extends Component {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`;

  render() {
    return (
      <fieldset>
        <legend>{title}</legend>
        <WrappedComponent {...this.props} />
      </fieldset>
    );
  }
};

以后咱们进行调用:

const WithHeaderDemo = withHeader(Demo,'高阶组件添加标题');

此时观察React DOM Tree

图片

能够看到,标题已经正确的进行了设置。

固然咱们也能够对其进行柯里化:

export default (title = '默认标题') => WrappedComponent => class HOC extends Component {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`;

  render() {
    return (
      <fieldset>
        <legend>{title}</legend>
        <WrappedComponent {...this.props} />
      </fieldset>
    );
  }
};

const WithHeaderDemo = withHeader('高阶组件添加标题')(Demo);

常见的HOC 实现方式

基于属性代理(Props Proxy)的方式

属性代理是最多见的高阶组件的使用方式,上面所说的高阶组件就是这种方式。
它经过作一些操做,将被包裹组件的props和新生成的props一块儿传递给此组件,这称之为属性代理。

export default function GenerateId(WrappedComponent) {
  return class HOC extends Component {
    static displayName = `PropsBorkerHOC(${getDisplayName(WrappedComponent)})`;

    render() {
      const newProps = {
        id: Math.random().toString(36).substring(2).toUpperCase()
      };

      return createElement(WrappedComponent, {
        ...this.props,
        ...newProps
      });
    }
  };
}

调用GenerateId:

const PropsBorkerDemo = GenerateId(Demo);

以后咱们观察React Dom Tree
图片
能够看到咱们经过 GenerateId 顺利的为 Demo 添加了 id

基于反向继承(Inheritance Inversion)的方式

首先来看一个简单的反向继承的例子:

export default function (WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    static displayName = `InheritanceHOC(${getDisplayName(WrappedComponent)})`;

    componentWillMount() {
      // 能够方便地获得state,作一些更深刻的修改。
      this.setState({
        innerText: '我被Inheritance修改了值'
      });
    }

    render() {
      return super.render();
    }
  };
}

如你所见返回的高阶组件类(Enhancer)继承了 WrappedComponent。而之因此被称为反向继承是由于 WrappedComponent 被动地被 Enhancer
继承,而不是 WrappedComponent 去继承 Enhancer。经过这种方式他们之间的关系倒转了。

反向继承容许高阶组件经过 this 关键词获取 WrappedComponent,意味着它能够获取到 stateprops,组件生命周期(Component Lifecycle)钩子,以及渲染方法(render)。深刻了解能够阅读__@Wenliang__文章中Inheritance Inversion(II)这一节的内容。

使用高阶组件遇到的问题

静态方法丢失

当使用高阶组件包装组件,原始组件被容器组件包裹,也就意味着新组件会丢失原始组件的全部静态方法。
下面为 Demo 添加一个静态方法:

Demo.getDisplayName = () => 'Demo';

以后调用 HOC

// 使用高阶组件
const WithHeaderDemo = HOC(Demo);

// 调用后的组件是没有 `getDisplayName` 方法的
typeof WithHeaderDemo.getDisplayName === 'undefined' // true

解决这个问题最简单(Yǘ Chǚn)的方法就是,将原始组件的全部静态方法所有拷贝给新组件:

export default (title = '默认标题') => (WrappedComponent) => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`;

    render() {
      return (
        <fieldset>
          <legend>{title}</legend>
          <WrappedComponent {...this.props} />
        </fieldset>
      );
    }
  }

 HOC.getDisplayName = WrappedComponent.getDisplayName;

  return HOC;
};

这样作,就须要你清楚的知道都有哪些静态方法须要拷贝的。或者你也但是使用hoist-non-react-statics来帮你自动处理,它会自动拷贝全部非React的静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics';

export default (title = '默认标题') => (WrappedComponent) => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`;

    render() {
      return (
        <fieldset>
          <legend>{title}</legend>
          <WrappedComponent {...this.props} />
        </fieldset>
      );
    }
  }

  // 拷贝静态方法
  hoistNonReactStatic(HOC, WrappedComponent);

  return HOC;
};

Refs属性不能传递

通常来讲,高阶组件能够传递全部的props属性给包裹的组件,可是不能传递 refs 引用。由于并非像 key 同样,refs 是一个伪属性,React 对它进行了特殊处理。
若是你向一个由高级组件建立的组件的元素添加 ref 应用,那么 ref 指向的是最外层容器组件实例的,而不是包裹组件。
但有的时候,咱们不可避免要使用 refs,官方给出的解决方案是:

传递一个ref回调函数属性,也就是给ref应用一个不一样的名字

同时还强调道:React在任什么时候候都不建议使用 ref应用
改写 Demo

class Demo extends Component {
  static propTypes = {
    getRef: PropTypes.func
  }

  static getDisplayName() {
    return 'Demo';
  }

  constructor(props) {
    super(props);
    this.state = {
      innerText: '我是一个普通组件'
    };
  }

  render() {
    const { getRef, ...props } = this.props;
    return (
      <div ref={getRef} {...props}>
        {this.state.innerText}
      </div>
    );
  }
}

以后咱们进行调用:

<WithHeaderDemo
  getRef={(ref) => {
    // 该回调函数被做为常规的props属性传递
    this.headerDemo = ref;
  }}
/>

虽然这并非最完美的解决方案,可是React官方说他们正在探索解决这个问题的方法,可以让咱们安心的使用高阶组件而没必要关注这个问题。

结语

这篇文章只是简单的介绍了高阶组件的两种最多见的使用方式:属性代理反向继承。以及高阶组件的常见问题。但愿经过本文的阅读使你对高阶组件有一个基本的认识。
写本文所产生的代码在study-hoc中。

本文做者:Godfery
本文同步发表于:HYPERS 前端博客

参考文章:

Higher-Order Components
深刻浅出React高阶组件
带着三个问题一块儿深刻浅出React高阶组件
阮一峰 - 高阶函数
深刻理解高阶组件

相关文章
相关标签/搜索