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感興趣的同學不妨去了解下。
購買地址:
噹噹
京東
歡迎關注我的公眾號:老幹部的大前端