Higher-Order Components (HOCs) are JavaScript functions which add functionality to existing component classes.javascript
經過函數向現有組件類添加邏輯,就是高階組件。html
讓咱們先來看一個多是史上最無聊的高階組件:java
function noId() { return function(Comp) { return class NoID extends Component { render() { const {id, ...others} = this.props; return ( <Comp {...others}/> ) } } } } const WithoutID = noId()(Comp);
這個例子向咱們展現了高階組件的工做方式:經過函數和閉包,改變已有組件的行爲——這裏是忽略id
屬性——而徹底不須要修改任何代碼。react
之因此稱之爲高階
,是由於在React中,這種嵌套關係會反映到組件樹上,層層嵌套就好像高階函數的function in function同樣,如圖:git
從圖上也能夠看出,組件樹雖然嵌套了多層,可是實際渲染的DOM結構並無改變。
若是你對這點有疑問,不妨本身寫寫例子試下,加深對React的理解。如今能夠先記下結論:咱們能夠放心的使用多層高階組件,甚至重複地調用,而沒必要擔憂影響輸出的DOM結構。es6
藉助函數的邏輯表現力,高階組件的用途幾乎是無窮無盡的:github
有的時候你須要替換一些已有組件,而新組件接收的參數和原組件並不徹底一致。編程
你能夠修改全部使用舊組件的代碼來保證傳入正確的參數——考慮改行吧若是你真這麼想redux
也能夠把新組件作一層封裝:segmentfault
class ListAdapter extends Component { mapProps(props) { return {/* new props */} } render() { return <NewList {...mapProps(this.props)} /> } }
若是有十個組件須要適配呢?若是你不想照着上面寫十遍,或許高階組件能夠給你答案
function mapProps(mapFn) { return function(Comp) { return class extends Component { render() { return <Comp {...mapFn(this.props)}/> } } } } const ListAdapter = mapProps(mapPropsForNewList)(NewList);
藉助高階組件,關注點被分離得更加乾淨:只須要關注真正重要的部分——屬性的mapping。
這個例子有些價值,卻仍然不夠打動人,若是你也這麼想,請往下看:
純組件易寫易測,越多越好,這是常識。然而在實際項目中,每每有許多的狀態和反作用須要處理,最多見的狀況就是異步了。
假設咱們須要異步加載一個用戶列表,一般的代碼多是這樣的:
class UserList extends Component { constructor(props) { super(); this.state = { list: [] } } componentDidMount() { loadUsers() .then(data=> this.setState({list: data.userList}) ) } render() { return ( <List list={this.state.list} /> ) } /* other bussiness logics */ }
實際狀況中,以上代碼每每還會和其它一些業務函數混雜在一塊兒——咱們建立了一個業務與反作用混雜的、有狀態的組件。
若是再來一個書單列表呢?再寫一個BookList而後把loadUsers改爲loadBooks ?
不只代碼重複,大量有狀態和反作用的組件,也使得應用更加難以測試。
也許你會考慮使用Flux。它確實能讓你的代碼更清晰,可是在有些場景下使用Flux就像大炮打蚊子。好比一個異步的下拉選擇框,若是要考慮複用的話,傳統的Flux/Reflux幾乎沒法優雅的處理,Redux稍好一些,但仍然很難作優雅。關於flux/redux的缺點不深刻,有興趣的能夠參考Cycle.js做者的文章
回到問題的本源:其實咱們只想要一個能複用的異步下拉列表而已啊!
高階函數試試?
import React, { Component } from 'react'; const DEFAULT_OPTIONS = { mapStateToProps: undefined, mapLoadingToProps: loading => ({ loading }), mapDataToProps: data => ({ data }), mapErrorToProps: error => ({ error }), }; export function connectPromise(options) { return (Comp) => { const finalOptions = { ...DEFAULT_OPTIONS, ...options, }; const { promiseLoader, mapLoadingToProps, mapStateToProps, mapDataToProps, mapErrorToProps, } = finalOptions; class AsyncComponent extends Component { constructor(props) { super(props); this.state = { loading: true, data: undefined, error: undefined, }; } componentDidMount() { promiseLoader(this.props) .then( data => this.setState({ data, loading: false }), error => this.setState({ error, loading: false }), ); } render() { const { data, error, loading } = this.state; const dataProps = data ? mapDataToProps(data) : undefined; const errorProps = error ? mapErrorToProps(error) : undefined; return ( <Comp {...mapLoadingToProps(loading)} {...dataProps} {...errorProps} {...this.props} /> ); } } return AsyncComponent; }; } const UserList = connectPromise({ promiseLoader: loadUsers, mapDataToProps: result=> ({list: result.userList}) })(List); //List can be a pure component const BookList = connectPromise({ promiseLoader: loadBooks, mapDataToProps: result=> ({list: result.bookList}) })(List);
不只大大減小了重複代碼,還把散落各處的異步邏輯裝進了能夠單獨管理和測試的籠子,在業務場景中,只須要純組件 + 配置
就能實現相同的功能——而不管是純組件
仍是配置
,都是對單元測試友好的,至少比異步組件友好多了。
高階組件的另外一個亮點,就是對函數式編程的友好。你可能已經注意到,目前我寫的全部高階函數,都是形如:
config => { return Component=> { return HighOrderCompoent } }
表示爲config=> Component=> Component
。
寫成嵌套的函數是爲了手動curry化,而參數的順序(爲何不是Component=> config=> Component
),則是爲了組合方便。關於curry與compose的使用,能夠移步個人另外一篇blog
舉個栗子,前面講了適配器和異步,咱們能夠很快就組合出二者的結合體:使用NewList的異步用戶列表
UserList = compose( connectPromise({ promiseLoader: loadUsers, mapResultToProps: result=> ({list: result.userList}) }), mapProps(mapPropsForNewList) )(NewList);
在團隊內部分享裏,個人總結是三個詞 Easy, Light-weight & Composable.
其實高階組件並非什麼新東西,本質上就是Decorator
模式在React的一種實現,但在至關一段時間內,這個優秀的模式都被人忽略。在我看來,大部分使用mixin
和class extends
的地方,高階組件都是更好的方案——畢竟組合優於繼承,而mixin——我的以爲沒資格參與討論。
使用高階組件還有兩個好處:
適用範圍廣,它不須要es6或者其它須要編譯的特性,有函數的地方,就有HOC。
Debug友好,它可以被React組件樹顯示,因此能夠很清楚地知道有多少層,每層作了什麼。相比之下不管是mixin仍是繼承,都顯得很是隱晦。
值得慶幸的是,社區也明顯注意到了高階組件的價值,不管是你們很是熟悉的react-redux 的connect
函數,仍是redux-form,高階組件的應用開始隨處可見。
下次當你想寫mixin
或class extends
的時候,不妨也考慮下高階組件。