高階組件的這種寫法的誕生來自於社區的實踐,目的是解決一些交叉問題(Cross-Cutting Concerns)。而最先時候 React
官方給出的解決方案是使用 mixin
。而 React 也在官網中寫道:javascript
We previously recommended mixins as a way to handle cross-cutting concerns. We've since realized that mixins create more trouble than they are worth.css
官方明顯也意識到了使用mixins
技術來解決此類問題所帶來的困擾遠高於其自己的價值。更多資料能夠查閱官方的說明。html
說到高階組件,就不得不先簡單的介紹一下高階函數。下面展現一個最簡單的高階函數前端
const add = (x,y,f) => f(x)+f(y)
當咱們調用add(-5, 6, Math.abs)
時,參數 x,y 和f 分別接收 -5,6 和 Math.abs
,根據函數定義,咱們能夠推導計算過程爲:java
x ==> -5 y ==> 6 f ==> abs f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11
用代碼驗證一下:react
add(-5, 6, Math.abs); //11
高階在維基百科的定義以下git
高階函數是至少知足下列一個條件的函數:github
接受一個或多個函數做爲輸入app
輸出一個函數dom
那麼,什麼是高階組件呢?類比高階函數的定義,高階組件就是接受一個組件做爲參數並返回一個新組件的函數。這裏須要注意高階組件是一個函數,並非組件,這一點必定要注意。
同時這裏強調一點高階組件自己並非 React
API。它只是一種模式,這種模式是由 React
自身的組合性質必然產生的。
更加通俗的講,高階組件經過包裹(wrapped)被傳入的React組件,通過一系列處理,最終返回一個相對加強(enhanced)的 React 組件,供其餘組件調用。
<!-- more -->
下面咱們來實現一個簡單的高階組件
export default WrappedComponent => class HOC extends Component { render() { return ( <fieldset> <legend>默認標題</legend> <WrappedComponent {...this.props} /> </fieldset> ); } };
在其餘組件中,咱們引用這個高階組件來強化它
export default class Demo extends Component { render() { return ( <div> 我是一個普通組件 </div> ); } } const WithHeaderDemo = withHeader(Demo);
下面咱們來看一下React DOM Tree
,調用了高階組件以後,發生了什麼:
能夠看到,Demo
被 HOC
包裹(wrapped)了以後添加了一個標題默認標題。可是一樣會發現,若是調用了多個 HOC
以後,咱們會看到不少的HOC
,因此應
該作一些優化,也就是在高階組件包裹(wrapped)之後,應該保留原有的名稱。
咱們改寫一下上述的高階組件代碼,增長一個 getDisplayName
函數,以後爲Demo
添加一個靜態屬性 displayName
。
const getDisplayName = component => component.displayName || component.name || 'Component'; export default WrappedComponent => class HOC extends Component { static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() { return ( <fieldset> <legend>默認標題</legend> <WrappedComponent {...this.props} /> </fieldset> ); } };
再次觀察React DOM Tree
能夠看到,該組件本來的名稱已經顯示在React DOM Tree
上了。
這個HOC 的功能是爲原有的組件添加一個標題,也就是說全部須要添加標題的組件均可以經過調用此 HOC 進行包裹(wrapped) 後實現此功能。
如今,咱們的 HOC
已經能夠爲其餘任意組件提供標題了,可是咱們還但願能夠修改標題中的字段。因爲咱們的高階組件是一個函數,因此能夠爲其添加一個參數title
。下面咱們對HOC
進行改寫:
export default (WrappedComponent, title = '默認標題') => class HOC extends Component { static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() { return ( <fieldset> <legend>{title}</legend> <WrappedComponent {...this.props} /> </fieldset> ); } };
以後咱們進行調用:
const WithHeaderDemo = withHeader(Demo,'高階組件添加標題');
此時觀察React DOM Tree
。
能夠看到,標題已經正確的進行了設置。
固然咱們也能夠對其進行柯里化:
export default (title = '默認標題') => WrappedComponent => class HOC extends Component { static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() { return ( <fieldset> <legend>{title}</legend> <WrappedComponent {...this.props} /> </fieldset> ); } }; const WithHeaderDemo = withHeader('高階組件添加標題')(Demo);
屬性代理是最多見的高階組件的使用方式,上面所說的高階組件就是這種方式。
它經過作一些操做,將被包裹組件的props
和新生成的props
一塊兒傳遞給此組件,這稱之爲屬性代理。
export default function GenerateId(WrappedComponent) { return class HOC extends Component { static displayName = `PropsBorkerHOC(${getDisplayName(WrappedComponent)})`; render() { const newProps = { id: Math.random().toString(36).substring(2).toUpperCase() }; return createElement(WrappedComponent, { ...this.props, ...newProps }); } }; }
調用GenerateId
:
const PropsBorkerDemo = GenerateId(Demo);
以後咱們觀察React Dom Tree
:
能夠看到咱們經過 GenerateId
順利的爲 Demo
添加了 id
。
首先來看一個簡單的反向繼承的例子:
export default function (WrappedComponent) { return class Enhancer extends WrappedComponent { static displayName = `InheritanceHOC(${getDisplayName(WrappedComponent)})`; componentWillMount() { // 能夠方便地獲得state,作一些更深刻的修改。 this.setState({ innerText: '我被Inheritance修改了值' }); } render() { return super.render(); } }; }
如你所見返回的高階組件類(Enhancer
)繼承了 WrappedComponent
。而之因此被稱爲反向繼承是由於 WrappedComponent
被動地被 Enhancer
繼承,而不是 WrappedComponent
去繼承 Enhancer
。經過這種方式他們之間的關係倒轉了。
反向繼承容許高階組件經過 this
關鍵詞獲取 WrappedComponent
,意味着它能夠獲取到 state
,props
,組件生命週期(Component Lifecycle)鉤子,以及渲染方法(render)。深刻了解能夠閱讀__@Wenliang__文章中Inheritance Inversion(II)
這一節的內容。
當使用高階組件包裝組件,原始組件被容器組件包裹,也就意味着新組件會丟失原始組件的全部靜態方法。
下面爲 Demo 添加一個靜態方法:
Demo.getDisplayName = () => 'Demo';
以後調用 HOC
:
// 使用高階組件 const WithHeaderDemo = HOC(Demo); // 調用後的組件是沒有 `getDisplayName` 方法的 typeof WithHeaderDemo.getDisplayName === 'undefined' // true
解決這個問題最簡單(Yǘ Chǚn)的方法就是,將原始組件的全部靜態方法所有拷貝給新組件:
export default (title = '默認標題') => (WrappedComponent) => { class HOC extends Component { static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() { return ( <fieldset> <legend>{title}</legend> <WrappedComponent {...this.props} /> </fieldset> ); } } HOC.getDisplayName = WrappedComponent.getDisplayName; return HOC; };
這樣作,就須要你清楚的知道都有哪些靜態方法須要拷貝的。或者你也但是使用hoist-non-react-statics來幫你自動處理,它會自動拷貝全部非React的靜態方法:
import hoistNonReactStatic from 'hoist-non-react-statics'; export default (title = '默認標題') => (WrappedComponent) => { class HOC extends Component { static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() { return ( <fieldset> <legend>{title}</legend> <WrappedComponent {...this.props} /> </fieldset> ); } } // 拷貝靜態方法 hoistNonReactStatic(HOC, WrappedComponent); return HOC; };
通常來講,高階組件能夠傳遞全部的props屬性給包裹的組件,可是不能傳遞 refs
引用。由於並非像 key
同樣,refs
是一個僞屬性,React
對它進行了特殊處理。
若是你向一個由高級組件建立的組件的元素添加 ref
應用,那麼 ref
指向的是最外層容器組件實例的,而不是包裹組件。
但有的時候,咱們不可避免要使用 refs
,官方給出的解決方案是:
傳遞一個ref回調函數屬性,也就是給ref應用一個不一樣的名字
同時還強調道:React在任什麼時候候都不建議使用 ref應用
改寫 Demo
class Demo extends Component { static propTypes = { getRef: PropTypes.func } static getDisplayName() { return 'Demo'; } constructor(props) { super(props); this.state = { innerText: '我是一個普通組件' }; } render() { const { getRef, ...props } = this.props; return ( <div ref={getRef} {...props}> {this.state.innerText} </div> ); } }
以後咱們進行調用:
<WithHeaderDemo getRef={(ref) => { // 該回調函數被做爲常規的props屬性傳遞 this.headerDemo = ref; }} />
雖然這並非最完美的解決方案,可是React
官方說他們正在探索解決這個問題的方法,可以讓咱們安心的使用高階組件而沒必要關注這個問題。
這篇文章只是簡單的介紹了高階組件的兩種最多見的使用方式:屬性代理
和反向繼承
。以及高階組件的常見問題。但願經過本文的閱讀使你對高階組件有一個基本的認識。
寫本文所產生的代碼在study-hoc中。
本文做者:Godfery
本文同步發表於:HYPERS 前端博客
參考文章:
Higher-Order Components
深刻淺出React高階組件
帶着三個問題一塊兒深刻淺出React高階組件
阮一峯 - 高階函數
深刻理解高階組件