路漫漫其修遠兮,吾將上下而求索。— 屈原《離騷》
寫在前面
高階元件不是React API的一部分,而是一種用來複用元件邏輯而衍生出來的一種技術
A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.
在討論高階元件元件之前,我們先來聊聊高階函式
高階函式
之前我寫過一篇文章 函式的柯里化與Redux中介軟體及applyMiddleware原始碼分析,在這邊文章裡我已經講過了什麼是高階函式,我們再來回顧一下,所謂的高階函式就是:
- 函式可以作為引數
- 函式可以作為返回值
如:
const debounce = (fn, delay) => {
let timeId = null
return () => {
timeId && clearTimeout(timeId)
timeId = setTimeout(fn, delay)
}
}
複製程式碼
高階函式的應用有很多,函式去抖,函式節流,bind函式,函式柯里化,map,Promise的then函式等
高階元件
高階元件的定義和高階函式有點像,但是要注意以下:
- 傳入一個元件
- 返回一個新元件
- 是一個函式
是不是感覺和高階函式很像,沒錯
總之,高階元件就是包裹(Wrapped)傳入的React元件,經過一系列處理,返回一個相對增強(Enhanced)的元件
react-redux中的connect函式就是高階元件很好的一個應用
如何編寫一個高階元件
下面通過一個例子來幫助大家編寫屬於自己的高階元件
現在我們有兩個元件,一個是UI元件Demo,用來顯示文字,一個是withHeader,它接受一個元件,然後返回一個新的元件,只不過給傳入的元件加上了一個標題,這個withHeader就是高階元件
class Demo extends Component {
render() {
return (
<div>
我是一個普通元件1
</div>
)
}
}
const withHeader = WrappedComponent => {
class HOC extends Component {
render() {
return (
<div>
<h1 className='demo-header'>我是標題</h1>
<WrappedComponent {...this.props}/>
</div>
)
}
}
return HOC
}
const EnhanceDemo = withHeader(Demo)
複製程式碼
結果如下:
HOC元件就是高階元件,它包裹了傳入的Demo元件,並給他新增了一個標題
假設有三個Demo元件,Demo1,Demo2,Demo3它們之間的區別就是元件的內容不一樣(這樣做是方便做演示),它們都用HOC進行包裹了,結果發現包裹之後的元件名稱都為HOC,這時候我們需要區分包裹之後的三個高階元件,
給HOC新增靜態displayName屬性
const getDisplayName = component => {
return component.displayName || component.name || 'Component'
}
const withHeader = WrappedComponent => {
class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
render() {
return (
<div>
<h1 className='demo-header'>我是標題</h1>
<WrappedComponent {...this.props}/>
</div>
)
}
}
return HOC
}
複製程式碼
再看看三個高階元件的名稱都不一樣了,但是我們想讓標題不寫死,而是可以動態傳入可以嗎?當然是可以的,我們可以藉助函式的柯里化實現,我們對withHeader改進下
const withHeader = title => WrappedComponent => {
class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
render() {
return (
<div>
<h1 className='demo-header'>{title}</h1>
<WrappedComponent {...this.props}/>
</div>
)
}
}
return HOC
}
const EnhanceDemo1 = withHeader('Demo1')(Demo1)
const EnhanceDemo2 = withHeader('Demo2')(Demo2)
const EnhanceDemo3 = withHeader('Demo3')(Demo3)
複製程式碼
結果如下:
我們可以藉助ES7的裝飾器來讓我們的寫法更簡潔
@withHeader('Demo1')
class Demo1 extends Component {
render() {
return (
<div>
我是一個普通元件1
</div>
)
}
}
@withHeader('Demo2')
class Demo2 extends Component {
render() {
return (
<div>
我是一個普通元件2
</div>
)
}
}
@withHeader('Demo3')
class Demo3 extends Component {
render() {
return (
<div>
我是一個普通元件3
</div>
)
}
}
class App extends Component {
render() {
return (
<Fragment>
<Demo1 />
<Demo2 />
<Demo3 />
</Fragment>
)
}
}
複製程式碼
關於裝飾器是什麼及怎麼使用,大家可自行查閱,後面我也專門寫一遍文章來講解它
到此為止,我們已經掌握瞭如何編寫一個高階元件,但是還沒完
兩種高階元件的實現方式
下面來說說高階元件的兩種實現方式:
- 屬性代理
- 反向繼承
屬性代理
屬性代理是最常見的方式,上面講的例子就是基於這種方式,只不過我們還可以寫的更完善些,我們可以在HOC中自定義一些屬性,然後和新生成的屬性一起傳給被包裹的元件,如下:
const withHeader = title => WrappedComponent => {
class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
render() {
const newProps = {
id: Math.random().toString(36).substring(2).toUpperCase()
}
return (
<div>
<h1 className='demo-header'>{title}</h1>
<WrappedComponent {...this.props} {...newProps}/>
</div>
)
}
}
return HOC
}
@withHeader('標題')
class Demo extends Component {
render() {
return (
<div style={this.props}>
{ this.props.children }
</div>
)
}
}
class App extends Component {
render() {
return (
<Fragment>
<Demo color='blue'>我是一個普通元件</Demo>
</Fragment>
)
}
}
複製程式碼
顯示如下:
對上面的高階元件和被包裹元件進行了改進,高階元件內部可以生成一個id屬性並傳入被包裹元件中,同時高階元件外部也可以接受屬性並傳入被包裹元件
反向繼承
我們們對上面的例子又做了一番修改:
我們讓HOC繼承wrappedComponent,這樣我們的HOC就擁有了wrappedComponent中定義的屬性和方法了,如state,props,生命週期函式
const withHeader = title => WrappedComponent => {
class HOC extends WrappedComponent {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
render() {
return (
<div>
<h1 className='demo-header'>{title}</h1>
{ super.render() }
</div>
)
}
componentDidMount() {
this.setState({
innerText: '我的值變成了2'
})
}
}
return HOC
}
複製程式碼
注意,HOC中的render函式,要呼叫父類的render函式,需要用到super關鍵字
相應的Demo元件也做了修改
@withHeader('標題')
class Demo extends Component {
constructor(props) {
super(props)
this.state = {
innerText: '我的初始值是1'
}
}
render() {
return (
<div style={this.props}>
{ this.state.innerText }
</div>
)
}
}
class App extends Component {
render() {
return (
<Fragment>
<Demo color='blue'></Demo>
</Fragment>
)
}
}
複製程式碼
最後顯示如下:
注意高階元件內部沒有了Demo元件,而是用原生的HTML標籤代替,可以對比上面的圖看出差異,為什麼沒有了Demo元件?因為我們是呼叫父類的render函式,而不是直接使用React元件
因為這種方式是讓我們的HOC繼承WrappedComponent,換句話也就是WrappedComponent被HOC繼承,所以稱為反向繼承
容易踩的坑
兩種繼承方式說完了,再說下書寫高階元件容易犯錯的地方,這也是官方文件需要我們注意的
不要在render函式中使用高階元件
這個需要大家對diff演算法有所瞭解,如果從 render 返回的元件等同於之前render函式返回的元件,React將會迭代地通過diff演算法更新子樹到新的子樹。如果不相等,則先前的子樹將會完全解除安裝。
而我們如果在render函式中使用高階元件,每次都會生成一個新的高階元件例項,這樣每次都會使對應的元件樹重新解除安裝,當然這樣肯定會有效能問題,但是不僅僅是效能問題,它還會造成元件的state和children全部丟失,這個才是致命的
靜態方法需手動複製
當我們在WrappedComponent定義的靜態方法,在高階元件例項上是找不到的
const withHeader = title => WrappedComponent => {
class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
render() {
return (
<div>
<h1 className='demo-header'>{title}</h1>
<WrappedComponent>
{ this.props.children }
</WrappedComponent>
</div>
)
}
}
return HOC
}
class Demo extends Component {
static hello() {
console.log('22')
}
render() {
return (
<div style={this.props}>
{ this.props.children }
</div>
)
}
}
const WithHeaderDemo = withHeader('標題')(Demo)
WithHeaderDemo.hello()
複製程式碼
我們有兩種辦法可以解決:
- 手動複製WrappedComponent的static方法到高階元件上
- 使用hoistNonReactStatic
第一種方法需要我們知道WrappedComponent上有哪些static方法,有一定的侷限性,通常我們使用第二種方法,但是需要我們安裝一個第三方庫:hoist-non-react-statics
import hoistNonReactStatic from 'hoist-non-react-statics'
const withHeader = title => WrappedComponent => {
class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
render() {
return (
<div>
<h1 className='demo-header'>{title}</h1>
<WrappedComponent>
{ this.props.children }
</WrappedComponent>
</div>
)
}
}
hoistNonReactStatic(HOC, WrappedComponent)
return HOC
}
複製程式碼
這樣就不會報錯了
Ref不會被傳遞
高階元件可以把所有屬性傳遞給被包裹元件,但是ref除外,因為ref不是一個真正的屬性,React 對它進行了特殊處理, 如果你向一個由高階元件建立的元件的元素新增ref應用,那麼ref指向的是最外層容器元件例項的,而不是包裹元件。
看一個例子就明白了
class App extends Component {
render() {
const WrappedComponentRef = React.createRef()
this.WrappedComponentRef = WrappedComponentRef
return (
<Fragment>
<WithHeaderDemo color='blue' ref={WrappedComponentRef}>
33333
</WithHeaderDemo>
</Fragment>
)
}
componentDidMount() {
console.log(this.WrappedComponentRef.current)
}
}
複製程式碼
結果列印的資訊如下:
我們的本意是把ref傳遞給內層包裹的WrappedComponent,結果列印的確是外層的HOC,我們再去看看React元件樹的資訊
ref作為了HOC的屬性並沒有傳遞到內部去,我想肯定是React對ref做了特殊的處理了,怎麼解決呢?簡單,換個名字不就可以了,我使用的是_ref
const withHeader = title => WrappedComponent => {
class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
render() {
return (
<div>
<h1 className='demo-header'>{title}</h1>
<WrappedComponent ref={this.props._ref}>
{ this.props.children }
</WrappedComponent>
</div>
)
}
}
hoistNonReactStatic(HOC, WrappedComponent)
return HOC
}
class App extends Component {
render() {
const WrappedComponentRef = React.createRef()
this.WrappedComponentRef = WrappedComponentRef
return (
<Fragment>
<WithHeaderDemo color='blue' _ref={WrappedComponentRef}>
33333
</WithHeaderDemo>
</Fragment>
)
}
componentDidMount() {
console.log(this.WrappedComponentRef.current)
}
}
複製程式碼
再來看看我們的列印結果
對應的React元件樹的情況
完美解決!
最後
本文只是簡單的介紹了下高階元件的書寫和其兩種實現方式,及一些要避免的坑,如果想對高階元件有個更系統的瞭解推薦去閱讀 React官方文件的高階元件,還有可以閱讀一些原始碼:如react-redux的connect,antd的Form.create
React學習之路很有很長
你們的打賞是我寫作的動力