React 深入系列6:高階元件

艾特老幹部發表於2019-03-04

React 深入系列,深入講解了React中的重點概念、特性和模式等,旨在幫助大家加深對React的理解,以及在專案中更加靈活地使用React。

1. 基本概念

高階元件是React 中一個很重要且比較複雜的概念,高階元件在很多第三方庫(如Redux)中都被經常使用。在專案中用好高階元件,可以顯著提高程式碼質量。

高階元件的定義類比於高階函式的定義。高階函式接收函式作為引數,並且返回值也是一個函式。類似的,高階元件接收React元件作為引數,並且返回一個新的React元件。高階元件本質上也是一個函式,並不是一個元件,這一點一定不要弄錯。

2. 應用場景

為什麼React引入高階元件的概念?它到底有何威力?讓我們先通過一個簡單的例子說明一下。

假設有一個元件MyComponent,需要從LocalStorage中獲取資料,然後渲染資料到介面。我們可以這樣寫元件程式碼:

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中的程式碼,這顯然是很冗餘的。下面讓我們來看看使用高階元件可以怎麼改寫這部分程式碼。

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這個高階元件包裝這些元件就可以了。

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

高階元件的引數並非只能是一個元件,它還可以接收其他引數。例如,元件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. 進階用法

高階元件最常見的函式簽名形式是這樣的:

HOC([param])([WrappedComponent])

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

import React, { Component } from 'react'

function 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進階之路》已上市,對React感興趣的同學不妨去了解下。 購買地址: 噹噹 京東

React 深入系列6:高階元件

歡迎關注我的公眾號:老幹部的大前端

alt

相關文章