1. 程式人生 > >聊聊React高階組件(Higher-Order Components)

聊聊React高階組件(Higher-Order Components)

出了 同時 tle tag 還在 span react super 反向

使用 react已經有不短的時間了,最近看到關於 react高階組件的一篇文章,看了之後頓時眼前一亮,對於我這種還在新手村晃蕩、一切朝著打怪升級看齊的小嘍啰來說,像這種難度不是太高同時門檻也不是那麽低的東西如今可不多見了啊,是個不可多得的 zhuangbility的利器,自然不可輕易錯過,遂深入了解了一番。


概述

高階組件的定義

React 官網上對高階組件的定義:

高階部件是一種用於復用組件邏輯的高級技術,它並不是 React API的一部分,而是從React 演化而來的一種模式。 具體地說,高階組件就是一個接收一個組件並返回另外一個新組件的函數。 相比於普通組件將 props 轉化成界面UI,高階組件將一個普通組件轉化為另外一個組件。

大概意思就是說, HOC並不是 reactAPI的一部分,而是一種實現的模式,有點類似於 觀察者模式單例模式之類的東西,本質還是函數。


功能

既然是能夠拿來 zhuangbility的利器,那麽不管怎麽說,簡單實用的招式必不可少,可以利用高階組件來做的事情:

  1. 代碼復用,邏輯抽象,抽離底層準備(bootstrap)代碼
  2. Props 更改
  3. State 抽象和更改
  4. 渲染劫持

用法示例

基本用法

  • 一個最簡單的高階組件(HOC) 示例如下:
     1 // HOCComponent.js
     2 
     3 import React from ‘react‘
     4 
     5 export default
    PackagedComponent => 6 class HOC extends React.Component { 7 render() { 8 return ( 9 <div id="HOCWrapper"> 10 <header> 11 <h1>Title</h1> 12 </header> 13 <PackagedComponent/> 14 </div> 15
    ) 16 } 17 }

  

此文件導出了一個函數,此函數返回經過一個經過處理的組件,它接受一個參數 PackagedComponent,此參數就是將要被 HOC包裝的普通組件,接受一個普通組件,返回另外一個新的組件,很符合高階組件的定義。

  • 此高階組件的簡單使用如下:
 1 // main.js
 2 import React from ‘react‘
 3 // (1)
 4 import HOCComponent from ‘./HOCComponent‘
 5 
 6 // (2)
 7 @HOCComponent 
 8 class Main extends React.Component {
 9   render() {
10     return(
11       <main>
12         <p>main content</p>
13       </main>
14     )
15   }
16 }
17 // (2)
18 // 也可以將上面的 @HOCComponent換成下面這句
19 //  const MainComponent = HOCComponent(Main)
20 export default MainComponent

想要使用高階組件,首先(1)將高階組件導入,然後(2)使用此組件包裝需要被包裝的普通組件 Main,這裏的@符號是 ES7中的decorator,寫過Java或者其他靜態語言的同學應該並不陌生,這實際上就是一個語法糖,可以使用 react-decorators 進行轉換, 在這裏相當於下面這句代碼:

const MainComponent = HOCComponent(Main)

@HOCComponent完全可以換成上面那句,只不過需要註意的是,類不具有提升的能力,所以若是覺得上面那句順眼換一下,那麽在換過之後,還要將這一句的位置移到類Main定義的後面。

最後,導出的是被高階組件處理過的組件 MainComponent

  • 這樣,就完成了一個普通組件的包裝,可以在頁面上將被包裝過的組件顯示出來了:
 1 import React from ‘react‘
 2 import { render } from ‘react-dom‘
 3 
 4 // 導入組件
 5 import MainComponent from ‘./main‘
 6 
 7 render(
 8   <MainComponent/>,
 9   document.getElementById(‘root‘)
10 )

頁面顯示如下:

技術分享

可以使用 React Developer Tools查看頁面結構:

技術分享

可以看出,組件Main的外面包裝了一層 HOC,有點類似於父組件和子組件,但很顯然高階組件並不等於父組件。

另外需要註意的一點, HOC這個高階組件,我們可能會用到不止一次,功能技術上沒什麽關系,但是不利於調試,為了快速地區分出某個普通組件的所屬的HOC到底是哪一個,我們可以給這些 HOC進行命名:

 1 // 獲取傳入的被包裝的組件名稱,以便為 HOC 進行命名
 2 let getDisplayName = component => {
 3   return component.displayName || component.name || ‘Component‘
 4 }
 5 
 6 export default PackagedComponent =>
 7   class HOC extends React.Component {
 8     // 這裏的 displayName就指的是 HOC的顯示名稱,我們將它重新定義了一遍
 9     // static被 stage-0  stage-1 和 stage-2所支持
10     static displayName = `HOC(${getDisplayName(PackagedComponent)})`
11     render() {
12       return (
13         <div id="HOCWrapper">
14           <header>
15             <h1>Title</h1>
16           </header>
17           <PackagedComponent/>
18         </div>
19       )
20     }
21   }

現在的 DOM結構:

技術分享

可以看到,原先的HOC已經變成了 HOC(Main)了,這麽做主要是利於我們的調試開發。

這裏的HOC,可以看做是一個簡單的為普通組件增加Title的高階組件,但是很明顯並不是所有的頁面都只使用同一個標題,標題必須要可定制化才符合實際情況。

想做到這一點也很簡單,那就是再為HOC組件的高階函數增加一個 title參數,另外考慮到 柯裏化 Curry函數和函數式編程,我們修改後的 HOC代碼如下:

 1 // HOCComponent.js
 2 
 3 // 增加了一個函數,這個函數存在一個參數,此參數就是要傳入的`title`
 4 export default PackagedComponent => componentTitle =>
 5   class HOC extends React.Component {
 6     static displayName = `HOC(${getDisplayName(PackagedComponent)})`
 7     render() {
 8       return (
 9         <div id="HOCWrapper">
10           <header>
11             <h1>{ componentTitle ? componentTitle : ‘Title‘ }</h1>
12           </header>
13           <PackagedComponent/>
14         </div>
15       )
16     }
17   }

使用方式如下:

1 // main.js
2 
3 // ...省略代碼
4 const MainComponent = HOCComponent(Main)(‘首頁‘)
5 export default MainComponent

然後在頁面上就可以看到效果了:

技術分享


屬性代理

HOC是包裹在普通組件外面的一層高階函數,任何要傳入普通組件內的props 或者 state 首先都要經過 HOC

propsstate等屬性原本是要流向 目標組件的腰包的,但是卻被 雁過拔毛的HOC攔路打劫,那麽最終這些 propsstates數據到底還能不能再到達 目標組件,或者哪些能到達以及到達多少就全由 HOC說了算了,也就是說,HOC擁有了提前對這些屬性進行修改的能力。

更改 Props

Props 的更改操作包括 增、刪、改、查,在修改和刪除 Props的時候需要註意,除非特殊要求,否則最好不要影響到原本傳遞給普通組件的 Props

 1 class HOC extends React.Component {
 2     static displayName = `HOC(${getDisplayName(PackagedComponent)})`
 3     render() {
 4       // 向普通組件增加了一個新的 `Props`
 5       const newProps = {
 6         summary: ‘這是內容‘
 7       }
 8       return (
 9         <div id="HOCWrapper">
10           <header>
11             <h1>{ componentTitle ? componentTitle : ‘Title‘ }</h1>
12           </header>
13           <PackagedComponent {...this.props} {...newProps}/>
14         </div>
15       )
16     }
17   }


通過 refs 獲取組件實例

普通組件如果帶有一個 ref屬性,當其通過 HOC的處理後,已經無法通過類似 this.refs.component的形式獲取到這個普通組件了,只會得到一個被處理之後的組件,想要仍然獲得原先的普通組件,需要對 ref進行處理,一種處理方法類似於 react-readux 中的 connect方法,如下:

 1 // HOCComponnet.js
 2 ...
 3 export default PackagedComponent => componentTitle =>
 4   class HOC extends React.Component {
 5     static displayName = `HOC(${getDisplayName(PackagedComponent)})`
 6     // 回調方法,當被包裝組件渲染完畢後,調用被包裝組件的 changeColor 方法
 7     propc(wrapperComponentInstance) {
 8       wrapperComponentInstance.changeColor()
 9     }
10     render() {
11       // 改變 props,使用 ref 獲取被包裝組件的示例,以調用其中的方法
12       const props = Object.assign({}, this.props, {ref: this.propc.bind(this)})
13       return (
14         <div id="HOCWrapper">
15           <header>
16             <h1>{ componentTitle ? componentTitle : ‘Title‘ }</h1>
17           </header>
18           <PackagedComponent {...props}/>
19         </div>
20       )
21     }
22   }

使用:

 1 // main.js
 2 ...
 3 class Main extends React.Component {
 4   render() {
 5     return(
 6       <main>
 7         <p>main content</p>
 8         <span>{ this.props.summary }</span>
 9       </main>
10     )
11   }
12   // main.js 中的changeColor 方法
13   changeColor() {
14     console.log(666);
15     document.querySelector(‘p‘).style.color = ‘greenyellow‘
16   }
17 }
18 ...


反向繼承(Inheritance Inversion)

相比於前面使用 HOC包裝在 普通組件外面的情況,反向繼承就是讓HOC繼承普通組件、打入普通組件的內部,這種更厲害,前面還只是攔路打劫,到了這裏就變成暗中潛伏了,這種情況下,普通組件變成了基類,而HOC變成了子類,子類能夠獲得父類所有公開的方法和字段。

反向繼承高階組件的功能:

  1. 能夠對普通組件生命周期內的所有鉤子函數進行覆寫
  2. 對普通組件的 state進行增刪改查的操作。
 1 // HOCInheritance.js
 2 
 3 let getDisplayName = (component)=> {
 4   return component.displayName || component.name || ‘Component‘
 5 }
 6 
 7 // (1)
 8 export default WrapperComponent =>
 9 class Inheritance extends WrapperComponent {
10   static displayName = `Inheritance(${getDisplayName(WrapperComponent)})`
11   // (2)
12   componentWillMount() {
13     this.state.name = ‘zhangsan‘
14     this.state.age = 18
15   }
16   render() {
17     // (4)
18     return super.render()
19   }
20   componentDidMount() {
21     // 5
22     super.componentDidMount()
23     // 6
24     document.querySelector(‘h1‘).style.color = ‘indianred‘
25   }
26 }

上述代碼中,讓 Inheritance 繼承 WrapperComponent (1)

並且覆寫了WrapperComponent 中的 componentWillMount函數(2)

在這個方法中對 WrapperComponentstate進行操作(3)

render方法中,為了防止破壞WrapperComponent原有的 render()方法,使用 superWrapperComponent 中原有的 render方法實現了一次(4)

componentDidMount同樣是先將 WrapperComponent 中的 componentDidMount方法實現了一次(5)

並且在原有的基礎上,又進行了一些額外的操作(6)

super並不是必須使用,這取決於你是否需要實現普通組件中原有的對應函數,一般來說都是需要的,類似於 mixin,至於到底是原有鉤子函數中的代碼先執行,還是 HOC中另加的代碼先執行,則取決於 super的位置,如果super在新增代碼之上,則原有代碼先執行,反之亦然。

另外,如果普通組件並沒有顯性實現某個鉤子函數,然後在HOC中又添加了這個鉤子函數,則 super不可用,因為並沒有什麽可以 super的,否則將報錯。

使用:

 1 // main2.js
 2 
 3 import React from ‘react‘
 4 import Inheritance from ‘./HOCInheritance‘
 5 
 6 class Main2 extends React.Component {
 7   state = {
 8     name: ‘wanger‘
 9   }
10   render() {
11     return (
12       <main>
13         <h1>summary of </h1>
14         <p>
15           my name is {this.state.name},
16           I‘m {this.state.age}
17         </p>
18       </main>
19     )
20   }
21 
22   componentDidMount() {
23     document.querySelector(‘h1‘).innerHTML += this.state.name
24   }
25 }
26 
27 const InheritanceInstace = Inheritance(Main2)
28 export default InheritanceInstace

頁面效果:

技術分享

可以看出,HOC為原有組件添加了 componentWillMount函數,在其中覆蓋了 Main2state的 ‘name‘屬性,並且其上添加了一個age屬性

HOC還將 MaincomponentDidMount方法實現了一次,並且在此基礎上,實現了自己的 componentDidMount方法。


用法拓展

HOC的用處很多,例如代替簡單的父組件傳遞props,修改組件的props數據等,除此之外,基於以上內容,我還想到了另外一種讓 HOC配合 redux的使用技巧。

用過vuevuex的人都知道,這兩個可謂是天作之合的一對好基友,後者基本上就是為前者量身定做,貼心的很,幾乎不用多做什麽事情,就能在 vue的任何組件中獲取存儲在 vuex中的數據,例如:

this.$store.state.data

只要 vuex中存儲了 data這個值,那麽一般情況下,在 vue的任何組件中,都是可以通過上面的一行代碼獲取到 data的。

至於,reactredux,看起來似乎和 vuevuex之間的關系差不多,用起來似乎也是二者搭配幹活不累,but,實際上他們之間的關系並沒有那麽鐵。

redux能夠搭配的東西不僅是react,還有 jqueryvueAngularEmber等任意框架,原生 jsok,頗有種搭天搭地搭空氣的傾向,所以,其與react之間肯定不可能像 vuevuex那麽融洽和諧。

技術分享

因而,如果你想在react中像在 vue中那麽毫不費力地通過類似於以下代碼在任意 react組件中獲取到 redux中的數據,那麽我只能說,你大概又寫了個 bug

this.$store.state.data

當然,如果你非要像這樣獲取到數據,也是可以的,但肯定要多費些手腳,一般在react中獲取 redux中數據的方法都要像這樣:

 1 // 首先,導入相關文件
 2 import { bindActionCreators } from ‘redux‘
 3 import { connect } from ‘react-redux‘
 4 import * as commonInfoActionsFromOtherFile from ‘actions/commoninfo.js‘
 5 
 6 // ...
 7 
 8 // 然後,傳遞數據和方法
 9 
10 let mapStateToProps = (state)=>{
11   return {
12     commonInfo: state.commonInfo
13   }
14 }
15 
16 let mapDispatchToProps = (dispatch)=>{
17   return {
18     commonInfoActions: bindActionCreators(commonInfoActionsFromOtherFile, dispatch)
19   }
20 }
21 // 最終,將組件導出
22 export default connect(
23   mapStateToProps,
24   mapDispatchToProps
25 )(ExampleComponent)

代碼其實也不是太多,但如果每次想要在一個組件獲取 redux中的數據和方法都要將這段代碼寫一遍,實在是有些啰嗦。

一種解決方法就是將 redux中所有的數據和 dispatch方法全都暴露給根組件,讓根組件往下傳遞到所有的子組件中,這確實是一種方法,但似乎有些冗余了, redux中的數據暴露在項目所有組件中,但有些組件根本用不到 redux中的數據,幹嘛還非要塞給它?

而另外一種方法,就是要用到本文所說的 HOC了。

既然高階組件能夠代理到 普通組件的Propsstate等屬性,那麽在使用諸如 redux等庫的時候,是不是可以讓高階組件來承接這些由 redux傳遞到全局的屬性,然後再用高階組件包裝普通組件,將獲得的屬性傳遞給普通組件,這樣普通組件就能獲取到 這些全局屬性了。

相比於使用 redux一個個地初始化所有需要使用到全局屬性的組件,使用高階組件作為載體,雖然結構上多了一層,但是操作上明顯方便簡化了許多。

理論上可行,但無圖無代碼,嘴上說說可沒用,我特地實驗了一番,已用實踐證實了其可行性。

一種封裝 HOC,讓其承載 redux 的示例代碼如下:

 1 // HocRedux.js
 2 
 3 import { bindActionCreators } from ‘redux‘
 4 import { connect } from ‘react-redux‘
 5 import * as actionsLists from ‘../actions/actionsLists‘
 6 
 7 let getDisplayName = component=> {
 8   return component.displayName || component.name || ‘Component‘
 9 }
10 
11 let mapStateToProps = (state)=>{
12   return {
13     reduxState: state
14   }
15 }
16 let mapDispatchToProps = (dispatch)=>{
17   return {
18     reduxActions: bindActionCreators(actionsLists, dispatch)
19   }
20 }
21 
22 export default ChildComponent =>
23 connect(
24   mapStateToProps,
25   mapDispatchToProps
26 )(class HocInheritance extends ChildComponent {
27   static displayName = `HocInheritance(${getDisplayName(ChildComponent)})`
28 })

然後,普通組件被此HOC處理後,就可以輕松獲取 redux中的數據了,想讓哪個組件獲取 redux,哪個組件就能獲取到,不想獲取的就獲取不到,簡單明了,使用方法和上面一樣:

1 import HocRedux from ‘HocRedux‘
2 // 省略代碼
3 const InheritanceInstace = Inheritance(Main2)
4 export default InheritanceInstace


註意事項

react官網 上還給出了幾條關於使用 HOC 時的註意事項。

  • 不要在render函數中使用高階組件

例如,以下就是錯誤示範:

1 // 這是個 render 方法
2 render() {
3   // 在 render 方法中使用了 HOC
4   // 每一次render函數調用都會創建一個新的EnhancedComponent實例
5   // EnhancedComponent1 !== EnhancedComponent2
6   const EnhancedComponent = enhance(MyComponent);
7   // 每一次都會使子對象樹完全被卸載或移除
8   return <EnhancedComponent />;
9 }

  • 靜態方法必須復制

HOC 雖然可以自動獲得 普通組件的 propsstate等屬性,但靜態方法必須要手動掛載。

1 // 定義靜態方法
2 WrappedComponent.staticMethod = function() {/*...*/}
3 // 使用高階組件
4 const EnhancedComponent = enhance(WrappedComponent);
5 
6 // 增強型組件沒有靜態方法
7 typeof EnhancedComponent.staticMethod === ‘undefined‘ // true

為了解決這個問題,在返回之前,可以向容器組件中復制原有的靜態方法:

1 function enhance(WrappedComponent) {
2   class Enhance extends React.Component {/*...*/}
3   // 必須得知道要拷貝的方法
4   Enhance.staticMethod = WrappedComponent.staticMethod;
5   return Enhance;
6 }

或者使用 hoist-non-react-statics來自動復制這些靜態方法

  • Refs不會被傳遞 對於 react組件來說,ref其實不是一個屬性,就像key一樣,盡管向其他props一樣傳遞到了組件中,但實際上在組件內時獲取不到的,它是由React特殊處理的。如果你給高階組件產生的組件的元素添加 ref,ref引用的是外層的容器組件的實例,而不是被包裹的組件。

想要解決這個問題,首先是盡量避免使用 ref,如果避免不了,那麽可以參照本文上面提到過的方法。

如果你喜歡我們的文章,關註我們的公眾號和我們互動吧。

技術分享

聊聊React高階組件(Higher-Order Components)