使用 react
已經有不短的時間了,最近看到關於 react
高階組件的一篇文章,看了之后頓時眼前一亮,對於我這種還在新手村晃盪、一切朝着打怪升級看齊的小嘍啰來說,像這種難度不是太高同時門檻也不是那么低的東西如今可不多見了啊,是個不可多得的 zhuangbility
的利器,自然不可輕易錯過,遂深入了解了一番。
概述
高階組件的定義
React 官網上對高階組件的定義:
高階部件是一種用於復用組件邏輯的高級技術,它並不是 React API的一部分,而是從
React
演化而來的一種模式。 具體地說,高階組件就是一個接收一個組件並返回另外一個新組件的函數。 相比於普通組件將props
轉化成界面UI,高階組件將一個普通組件轉化為另外一個組件。
大概意思就是說, HOC
並不是 react
API
的一部分,而是一種實現的模式,有點類似於 觀察者模式
、單例模式
之類的東西,本質還是函數。
功能
既然是能夠拿來 zhuangbility
的利器,那么不管怎么說,簡單實用的招式必不可少,可以利用高階組件來做的事情:
- 代碼復用,邏輯抽象,抽離底層准備(
bootstrap
)代碼Props
更改State
抽象和更改- 渲染劫持
用法示例
基本用法
- 一個最簡單的高階組件(
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
。
props
和 state
等屬性原本是要流向 目標組件的腰包的,但是卻被 雁過拔毛的HOC
攔路打劫,那么最終這些 props
和 states
數據到底還能不能再到達 目標組件,或者哪些能到達以及到達多少就全由 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
變成了子類,子類能夠獲得父類所有公開的方法和字段。
反向繼承高階組件的功能:
- 能夠對普通組件生命周期內的所有鈎子函數進行覆寫
- 對普通組件的
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)
在這個方法中對 WrapperComponent
的 state
進行操作(3)
在 render
方法中,為了防止破壞WrapperComponent
原有的 render()
方法,使用 super
將 WrapperComponent
中原有的 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
函數,在其中覆蓋了 Main2
中 state
的 'name'屬性,並且其上添加了一個age
屬性
HOC
還將 Main
的 componentDidMount
方法實現了一次,並且在此基礎上,實現了自己的 componentDidMount
方法。
用法拓展
HOC
的用處很多,例如代替簡單的父組件傳遞props
,修改組件的props
數據等,除此之外,基於以上內容,我還想到了另外一種讓 HOC
配合 redux
的使用技巧。
用過vue
與 vuex
的人都知道,這兩個可謂是天作之合的一對好基友,后者基本上就是為前者量身定做,貼心的很,幾乎不用多做什么事情,就能在 vue
的任何組件中獲取存儲在 vuex
中的數據,例如:
this.$store.state.data
只要 vuex
中存儲了 data
這個值,那么一般情況下,在 vue
的任何組件中,都是可以通過上面的一行代碼獲取到 data
的。
至於,react
和 redux
,看起來似乎和 vue
與vuex
之間的關系差不多,用起來似乎也是二者搭配干活不累,but
,實際上他們之間的關系並沒有那么鐵。
redux
能夠搭配的東西不僅是react
,還有 jquery
、vue
、Angular
、Ember
等任意框架,原生 js
也 ok
,頗有種搭天搭地搭空氣的傾向,所以,其與react
之間肯定不可能像 vue
與vuex
那么融洽和諧。
因而,如果你想在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
了。
既然高階組件能夠代理到 普通組件的
Props
和state
等屬性,那么在使用諸如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
雖然可以自動獲得 普通組件的 props
和 state
等屬性,但靜態方法必須要手動掛載。
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
,如果避免不了,那么可以參照本文上面提到過的方法。
如果你喜歡我們的文章,關注我們的公眾號和我們互動吧。