React 深刻系列6:高階組件

React 深刻系列,深刻講解了React中的重點概念、特性和模式等,旨在幫助你們加深對React的理解,以及在項目中更加靈活地使用React。

1. 基本概念

高階組件是React 中一個很重要且比較複雜的概念,高階組件在不少第三方庫(如Redux)中都被常用。在項目中用好高階組件,能夠顯著提升代碼質量。html

高階組件的定義類比於高階函數的定義。高階函數接收函數做爲參數,而且返回值也是一個函數。相似的,高階組件接收React組件做爲參數,而且返回一個新的React組件。高階組件本質上也是一個函數,並非一個組件,這一點必定不要弄錯。react

2. 應用場景

爲何React引入高階組件的概念?它到底有何威力?讓咱們先經過一個簡單的例子說明一下。git

假設有一個組件MyComponent,須要從LocalStorage中獲取數據,而後渲染數據到界面。咱們能夠這樣寫組件代碼:github

import React, { Component } from 'react'

class MyComponent extends Component {

  componentWillMount() {
      let data = localStorage.getItem('data');
      this.setState({data});
  }
  
  render() {
    return <div>{this.state.data}</div>
  }
}

代碼很簡單,但當有其餘組件也須要從LocalStorage中獲取一樣的數據展現出來時,須要在每一個組件都重複componentWillMount中的代碼,這顯然是很冗餘的。下面讓咱們來看看使用高階組件能夠怎麼改寫這部分代碼。redux

import React, { Component } from 'react'

function withPersistentData(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem('data');
        this.setState({data});
    }
    
    render() {
      // 經過{...this.props} 把傳遞給當前組件的屬性繼續傳遞給被包裝的組件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
}

const MyComponentWithPersistentData = withPersistentData(MyComponent2)

withPersistentData就是一個高階組件,它返回一個新的組件,在新組件的componentWillMount中統一處理從LocalStorage中獲取數據的邏輯,而後將獲取到的數據以屬性的方式傳遞給被包裝的組件WrappedComponent,這樣在WrappedComponent中就能夠直接使用this.props.data獲取須要展現的數據了,如MyComponent2所示。當有其餘的組件也須要這段邏輯時,繼續使用withPersistentData這個高階組件包裝這些組件就能夠了。設計模式

經過這個例子,能夠看出高階組件的主要功能是封裝並分離組件的通用邏輯,讓通用邏輯在組件間更好地被複用。高階組件的這種實現方式,本質上是一個裝飾者設計模式。app

高階組件的參數並不是只能是一個組件,它還能夠接收其餘參數。例如,組件MyComponent3須要從LocalStorage中獲取key等於name的數據,而不是上面例子中寫死的key等於data的數據,withPersistentData這個高階組件就不知足咱們的需求了。咱們可讓它接收額外的一個參數,來決定從LocalStorage中獲取哪一個數據:函數

import React, { Component } from 'react'

function withPersistentData(WrappedComponent, key) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
        this.setState({data});
    }
    
    render() {
      // 經過{...this.props} 把傳遞給當前組件的屬性繼續傳遞給被包裝的組件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
  
  //省略其餘邏輯...
}

class MyComponent3 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
  
  //省略其餘邏輯...
}

const MyComponent2WithPersistentData = withPersistentData(MyComponent2, 'data');
const MyComponent3WithPersistentData = withPersistentData(MyComponent3, 'name');

新版本的withPersistentData就知足咱們獲取不一樣key的值的需求了。高階組件中的參數固然也能夠是函數,咱們將在下一節進一步說明。工具

3. 進階用法

高階組件最多見的函數簽名形式是這樣的:this

HOC([param])([WrappedComponent])

用這種形式改寫withPersistentData,以下:

import React, { Component } from 'react'

const withPersistentData = (key) => (WrappedComponent) => {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
        this.setState({data});
    }
    
    render() {
      // 經過{...this.props} 把傳遞給當前組件的屬性繼續傳遞給被包裝的組件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
  
  //省略其餘邏輯...
}

class MyComponent3 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
  
  //省略其餘邏輯...
}

const MyComponent2WithPersistentData = withPersistentData('data')(MyComponent2);
const MyComponent3WithPersistentData = withPersistentData('name')(MyComponent3);

實際上,此時的withPersistentData和咱們最初對高階組件的定義已經不一樣。它已經變成了一個高階函數,但這個高階函數的返回值是一個高階組件。HOC([param])([WrappedComponent])這種形式中,HOC([param])纔是真正的高階組件,咱們能夠把它當作高階組件的變種形式。這種形式的高階組件因其特有的便利性——結構清晰(普通參數和被包裹組件分離)、易於組合,大量出如今第三方庫中。如react-redux中的connect就是一個典型。connect的定義以下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(WrappedComponent)

這個函數會將一個React組件鏈接到Redux 的 store。在鏈接的過程當中,connect經過函數類型的參數mapStateToProps,從全局store中取出當前組件須要的state,並把state轉化成當前組件的props;同時經過函數類型的參數mapDispatchToProps,把當前組件用到的Redux的action creators,以props的方式傳遞給當前組件。

例如,咱們把組件ComponentA鏈接到Redux上的寫法相似於:

const ConnectedComponentA = connect(mapStateToProps, mapDispatchToProps)(ComponentA);

咱們能夠把它拆分來看:

// connect 是一個函數,返回值enhance也是一個函數
const enhance = connect(mapStateToProps, mapDispatchToProps);
// enhance是一個高階組件
const ConnectedComponentA = enhance(ComponentA);

當多個函數的輸出和它的輸入類型相同時,這些函數是很容易組合到一塊兒使用的。例如,有f,g,h三個高階組件,都只接受一個組件做爲參數,因而咱們能夠很方便的嵌套使用它們:f( g( h(WrappedComponent) ) )。這裏能夠有一個例外,即最內層的高階組件h能夠有多個參數,但其餘高階組件必須只能接收一個參數,只有這樣才能保證內層的函數返回值和外層的函數參數數量一致(都只有1個)。

例如咱們將connect和另外一個打印日誌的高階組件withLog聯合使用:

const ConnectedComponentA = connect(mapStateToProps)(withLog(ComponentA));

這裏咱們定義一個工具函數:compose(...functions),調用compose(f, g, h) 等價於 (...args) => f(g(h(...args)))。用compose函數咱們能夠把高階組件嵌套的寫法打平:

const enhance = compose(
  connect(mapStateToProps),
  withLog
);
const ConnectedComponentA = enhance(ComponentA);

像Redux等不少第三方庫都提供了compose的實現,compose結合高階組件使用,能夠顯著提升代碼的可讀性和邏輯的清晰度。

4.與父組件區別

有些同窗可能會以爲高階組件有些相似父組件的使用。例如,咱們徹底能夠把高階組件中的邏輯放到一個父組件中去執行,執行完成的結果再傳遞給子組件。從邏輯的執行流程上來看,高階組件確實和父組件比較相像,可是高階組件強調的是邏輯的抽象。高階組件是一個函數,函數關注的是邏輯;父組件是一個組件,組件主要關注的是UI/DOM。若是邏輯是與DOM直接相關的,那麼這部分邏輯適合放到父組件中實現;若是邏輯是與DOM不直接相關的,那麼這部分邏輯適合使用高階組件抽象,如數據校驗、請求發送等。

5. 注意事項

1)不要在組件的render方法中使用高階組件,儘可能也不要在組件的其餘生命週期方法中使用高階組件。由於高階組件每次都會返回一個新的組件,在render中使用會致使每次渲染出來的組件都不相等(===),因而每次render,組件都會卸載(unmount),而後從新掛載(mount),既影響了效率,又丟失了組件及其子組件的狀態。高階組件最適合使用的地方是在組件定義的外部,這樣就不會受到組件生命週期的影響了。

2)若是須要使用被包裝組件的靜態方法,那麼必須手動拷貝這些靜態方法。由於高階組件返回的新組件,是不包含被包裝組件的靜態方法。hoist-non-react-statics能夠幫助咱們方便的拷貝組件全部的自定義靜態方法。有興趣的同窗能夠自行了解。

3)Refs不會被傳遞給被包裝組件。儘管在定義高階組件時,咱們會把全部的屬性都傳遞給被包裝組件,可是ref並不會傳遞給被包裝組件。若是你在高階組件的返回組件中定義了ref,那麼它指向的是這個返回的新組件,而不是內部被包裝的組件。若是你但願獲取被包裝組件的引用,你能夠把ref的回調函數定義成一個普通屬性(給它一個ref之外的名字)。下面的例子就用inputRef這個屬性名代替了常規的ref命名:

function FocusInput({ inputRef, ...rest }) {
  return <input ref={inputRef} {...rest} />;
}

//enhance 是一個高階組件
const EnhanceInput = enhance(FocusInput);

// 在一個組件的render方法中...
return (<EnhanceInput 
  inputRef={(input) => {
    this.input = input
  }
}>)

// 讓FocusInput自動獲取焦點
this.input.focus();

下篇預告:

React 深刻系列7:React 經常使用模式


個人新書《React進階之路》已上市,請你們多多支持!
連接:京東 噹噹

圖片描述

相關文章
相關標籤/搜索