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高階組件
阮一峯 - 高階函數
深刻理解高階組件

相關文章
相關標籤/搜索