React 深刻系列,深刻講解了React中的重點概念、特性和模式等,旨在幫助你們加深對React的理解,以及在項目中更加靈活地使用React。
高階組件是React 中一個很重要且比較複雜的概念,高階組件在不少第三方庫(如Redux)中都被常用。在項目中用好高階組件,能夠顯著提升代碼質量。html
高階組件的定義類比於高階函數的定義。高階函數接收函數做爲參數,而且返回值也是一個函數。相似的,高階組件接收React組件做爲參數,而且返回一個新的React組件。高階組件本質上也是一個函數,並非一個組件,這一點必定不要弄錯。react
爲何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的值的需求了。高階組件中的參數固然也能夠是函數,咱們將在下一節進一步說明。工具
高階組件最多見的函數簽名形式是這樣的: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
結合高階組件使用,能夠顯著提升代碼的可讀性和邏輯的清晰度。
有些同窗可能會以爲高階組件有些相似父組件的使用。例如,咱們徹底能夠把高階組件中的邏輯放到一個父組件中去執行,執行完成的結果再傳遞給子組件。從邏輯的執行流程上來看,高階組件確實和父組件比較相像,可是高階組件強調的是邏輯的抽象。高階組件是一個函數,函數關注的是邏輯;父組件是一個組件,組件主要關注的是UI/DOM。若是邏輯是與DOM直接相關的,那麼這部分邏輯適合放到父組件中實現;若是邏輯是與DOM不直接相關的,那麼這部分邏輯適合使用高階組件抽象,如數據校驗、請求發送等。
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進階之路》已上市,請你們多多支持!
連接:京東 噹噹