前言
學習react已經有一段時間了,期間在閱讀官方文檔的基礎上也看了不少文章,但感覺對很多東西的理解還是不夠深刻,因此這段時間又在擼一個基于react全家桶的聊天App(現在還在瞎78寫的階段,在往這個聊天App這個方向寫),通過實踐倒是對react相關技術棧有了更為深刻的理解,而在使用react-redux的過程中,發現connect好像還挺有意思的,也真實感受到了高階組件所帶來的便利,出于自己寫項目本身就是為了學習的目的,因此對高階組件又進行了一番學習。寫下這篇文章主要是對高階組件的知識進行一個梳理與總結,如有錯誤疏漏之處,敬請指出,不勝感激。
初識高階組件
要學習高階組件首先我們要知道的就是高階組件是什么,解決了什么樣的問題。
React官方文檔的對高階組件的說明是這樣的:
A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, perse. They are a pattern that emerges from React’s compositional nature.
從上面的說明我們可以看出,react的高階組件并不是react API的一部分。它源自于react的生態。
簡單來說,一個高階組件就是一個函數,它接受一個組件作為輸入,然后會返回一個新的組件作為結果,且所返回的新組件會進行相對增強。值得注意的是,我們在這說的組件并不是組件實例,而是一個組件類或者一個無狀態組件的函數。就像這樣:
import React from 'react'
function removeUserProp(WrappedComponent) {
//WrappingComponent這個組件名字并不重要,它至少一個局部變量,繼承自React.Component
return class WrappingComponent extends React.Component {
render() {
// ES6的語法,可以將一個對象的特定字段過濾掉
const {user, ...otherProps} = this.props
return <WrappedComponent {...otherProps} />
}
}
}
了解設計模式的大佬們應該發現了,它其實就是的設計模式中的裝飾者模式在react中的應用,它通過組合的方式從而達到很高的靈活程度和復用。
就像上面的代碼,我們定義了一個叫做 removeUserProp 的高階組件,傳入一個叫做 WrappedComponent 的參數(代表一個組件類),然后返回一個新的組件 ,新的組件與原組件并沒有太大的區別,只是將原組件中的 prop 值 user 給剔除了出來。
有了上面這個高階組件的,當我們不希望某個組件接收到 user 時,我們就可以將這個組件作為參數傳入 removeUserProp() 函數中,然后使用這個返回的組件就行了:
const NewComponent = removeUserProp(OldComponent)
這樣 NewComponent 組件與 OldComponent 組件擁有完全一樣的行為,唯一的區別就在于傳入的name屬性對這個組件沒有任何作用,它會自動屏蔽這個屬性。也就是說,我們這個高階組件成功的為傳入的組件增加了一個屏蔽某個prop的功能。
那么明白了什么是高階組件后,我們接下來要做的是,弄清楚高階組件主要解決的問題,或者說我們為什么需要高階組件?總結起來主要是以下兩個方面:
- 代碼重用
在很多情況下,react組件都需要共用同一個邏輯,我們在這個時候就可以把這部分共用的邏輯提取出來,然后利用高階組件的形式將其組合,從而減少很多重復的組件代碼。
2.修改React組件的行為
很多時候有些現成的react組件并不是我們自己擼出來的,而是來自于GitHub上的大佬們的開源貢獻,而當我們要對這些組件進行復用的時候,我們往往都不想去觸碰這些組件的內部邏輯,這時我們就能通過高階組件產生新的組件滿足自身需求,同時也對原組件沒有任何損害。
現在我們對高階組件有了一個較為直觀的認識,知道了什么是高階組件以及高階組件的主要用途。接下來我們就要具體了解高階組件的實現方式以及它的具體用途了。
高階組件的實現分類
對于高階組件的實現方式我們可以根據作為參數傳入的組件與返回的新組件的關系將高階組件的實現方式分為以下兩大類:
- 代理方式的高階組件
- 繼承方式的高階組件
代理方式的高階組件
從高階組件的使用頻率來講,我們使用的絕大多數的高階組件都是代理方式的高階組件,如react-redux中的connect,還有我們在上面所實現的那個removeUserProp。這類高階組件的特點是返回的新組件類直接繼承于 React.Component 類。新組建在其中扮演的角色是一個傳入參數組件的代理,在新組建的render函數中,把被包裹的組件渲染出來。在此過程中,除了高階組件自己需要做的工作,其他的工作都會交給被包裹的組件去完成。
代理方式的高階組件具體而言,應用場景可以分為以下幾個:
- 操作prop
- 通過ref獲取組件實例
- 抽取狀態
- 包裝組件
控制prop
代理類型的高階組件返回的新組件時,渲染過程也會被新組建的render函數所控制,而在此過程中,render函數相對于一個代理,完全決定該如何使用被包裹在其中的組件。在render函數中,this.props包含了新組件接受到的所有prop。因此最直觀的用法就是接受到props,然后進行任何讀取,增減,修改等控制props的自定義操作。
就比如我們上面的那個示例,就做到了刪除prop的功能,當然我們也能實現一個添加prop的高階組件:
function addNewProp(WrappedComponent, newProps) {
return class WrappingComponent extends React.Component {
render() {
return <WrappedComponent {...thisProps} {...newProps} />
}
}
}
這個addNewProp高階組件與我們最開始舉例的removeUserProp高階組件在實現上并無太大的區別。唯一區別較大的就是我們傳入的參數除了WrappedComponent組件類外,還新增了newProps參數。這樣的高階組件在復用性方面會跟友好,我們可以利用這樣一個高階組件給不同的組件添加不同的新屬性,比如這樣:
const FirstComponent = addNewProp(OldComponent,{num: First})
const LastComponent = addNewProp(NewComponent,{num: Last})
在上面的代碼中,我們實現了讓兩個完全不同的組件分別通過高階組件生成了兩個完成不同的新的組件,而這其中唯一相同的是都添加了一個屬性值,且這個屬性還不相同。從上面的代碼我們也不難發現,高階組件可以重用在不同組件上,減少了重復的代碼。當需要注意的是,在修改和刪除 Props的時候,除非由特殊的要求,否則最好不要影響到原本傳遞給普通組件的 Props。
通過ref獲取組件實例
我們可以通過ref獲取組件實例,但值得注意的是,React官方不提倡訪問ref,我們只是討論一下這個技術的可行性。在此我們寫一個refsHOC的高階組件,可以獲得被包裹組件的ref,從而根據ref直接操縱被包裹組件的實例:
import React from 'react'
function refsHOC(WrappedComponent) => {
return class HOCComponent extends React.Component {
constructor() {
super(...arguments)
this.linkRef = this.linkRef.bind(this)
}
linkRef(wrappedInstance) {
this._root = wrappedInstance
}
render() {
const props = {...this.props, ref: this.linkRef}
return <WrappedComponent {...props}/>
}
}
}
export default refsHOC
這個refs高階組件的工作原理其實也是增加傳遞給被包裹組件的props,不同的是利用了ref這個特殊的prop而已。我們通過linkRef來給被包裹組件傳遞ref值,linkRef被調用時,我們就可以得到被包裹組件的DOM實例。
這種高階組件在用途上來講可以說是無所不能的,因為只要能夠獲得對被包裹組件的引用,就能通過這個引用任意操縱一個組件的DOM元素,賊酸爽。但它從某個角度來講也是啥也干不了的,因為react團隊表示:不要過度使用 Refs。且我們也有更好的替代品——控制組件(Controlled Component)來解決相關問題,因此這個坑建議大家還是盡量少踩為好。
抽取狀態
對于抽取狀態,我想大家應該都不會很陌生。react-redux中的connect函數就實現了這種功能,它異常的強大,也成功吸引了我對高階組件的注意力。但在這有一點需要明確的是:connect函數本身并不是高階組件,connect函數執行的結果才是一個高階組件。讓我們來看看connect的源碼的主要邏輯:
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
constructor(props, context) {
//參數獲取
super(props, context)
this.store = props.store || context.store
const storeState = this.store.getState()
this.state = { storeState }
}
// 進行判斷,當數據發生改變時,Component重新渲染
shouldComponentUpdate(nextProps, nextState) {
if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
this.updateState(nextProps)
return true
}
}
// 改變Component中的state
componentDidMount() {
this.store.subscribe(() = {
this.setState({
storeState: this.store.getState()
})
})
}
render(){
this.renderedElement = createElement(WrappedComponent,
this.mergedProps
)
return this.renderedElement
}
}
return hoistStatics(Connect, WrappedComponent)
}
}
從上面的代碼我們不難看出connect模塊的返回值wrapWithConnect是一個函數,而這個函數才是我們所認知的高階組件。wrapWithConnect函數會返回一個ReactComponent對象Connect,Connect會重新render外部傳入的原組件WrappedComponent,并把connect中所傳入的mapStateToProps, mapDispatchToProps和this.props合并后結合成一個對象,通過屬性的方式傳給WrappedComponent,這才是最終的渲染結果。
包裝組件
在日常開發中我們所接觸到的大多數的高階組件都是通過修改props部分來對輸入的組件進行相對增強的。但其實高階組件還有其他的方式來增強組件,比如我們可以通過在render函數中的JSX引入其他元素,甚至將多個react組件合并起來,來獲得更騷氣的樣式或方法,例如我們可以給組件增加style來改變組件樣式:
const styleHOC = (WrappedComponent, style) => {
return class HOCComponent extends React.Component {
render() {
return (
<div style={style}>
<WrappedComponent {...this.props} />
</div>
)
}
}
}
當我們想改變組件的樣式的時候,我們就可以直接調用這個函數,比如這樣:
const style = {
background-color: #f1fafa;
font-family: "微軟雅黑";
font-size: 20px;
}
const BeautifulComponent = styleHOC(uglyComponent, style)
繼承方式的高階組件
前面我們討論了代理方式實現的高階組件以及它們的主要使用方式,現在我們繼續來討論一下以繼承方式實現的高階組件。
。繼承方式的高階組件通過繼承來關聯作為參數傳入的組件和返回的組件,比如傳入的組件參數是OldComponent,那函數所返回的組件就直接繼承于OldComponemt。
碼界有句老話說的好:組合優于繼承。在高階組件里也不例外。
繼承方式的高階組件相對于代理方式的高階組件有很多不足之處,比如輸入的組件與輸出的組件共有一個生命周期等,因此通常我們接觸到的高階組件大多是代理方式實現的高階組件,也推薦大家首先考慮以代理方式來實現高階組件。但我們還是需要去了解并學習它,畢竟它也是有可取之處的,比如在操作生命周期函數上它還是具有其優越性的。
操作生命周期函數
說繼承方式的高階組件在操縱生命周期函數上有其優越性其實不夠說明它在這個領域的地位,更準確地表達是:操作生命周期函數是繼承方式的高階組件所特有的功能。這是由于繼承方式的高階組件返回的新組件繼承于作為參數傳入的組件,兩個組件的生命周期是共用的,因此可以重新定義組件的生命周期函數并作用于新組件。而代理方式的高階組件作為參數輸入的組件與輸出的組件完全是兩個生命周期,因此改變生命周期函數也就無從說起了。
例如我們可以定義一個讓參數組件只有在用戶登錄時才顯示的高階組件:
const shouldLoggedInHOC = (WrappedComponent) => {
return class MyComponent extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render()
}
else {
return null
}
}
}
}
操縱Prop
除了操作生命周期函數外,繼承方式的高階函數也能對Prop進行操作,但總的難說賊麻煩,當然也有簡單的方式,比如這樣:
function removeProps(WrappedComponent) {
return class NewComponent extends WrappedComponent {
render() {
const{ user, ...otherProps } = this.props
this.props = otherProps
return super.render()
}
}
}
雖然這樣看起來很簡單,但我們直接修改了this.props,這不是一個好的實踐,可能會產生不可預料的后果,更好的操作辦法是這樣的:
function removeProps(WrappedComponent) {
return class NewComponent extends WrappedComponent {
render() {
const element =super.render()
const{ user, ...otherProps } = this.props
this.props = otherProps
return React.cloneElement(element, this.props, element.props.children)
}
}
}
我們可以通過React.cloneElement來傳入新的props,讓這些產生的組件重新渲染一次。但雖然這種方式可以解決直接修改this.props所帶來的問題,但實現起來賊麻煩,唯一用得上的就是高階組件需要根據參數組件WrappedComponent渲染結果來決定如何修改props時用得上,其他的時候顯然使用代理模式更便捷清晰。
高階組件命名
用 HOC 包裹了一個組件會使它失去原本 WrappedComponent 的名字,可能會影響開發和debug。
因此我們通常會用 WrappedComponent 的名字加上一些 前綴作為 HOC 的名字。我們來看看React-Redux是怎么做的:
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}
HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//或
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
...
}
實際上我們不用自己來寫getDisplayName這個函數,recompose 提供了這個函數,我們只要使用即可。
結尾語
我們其他要注意的就是官方文檔所說的幾個約定與相關規范,在此我就不一一贅述了,感興趣的可以自己去看看。最后很感謝能看到這里的朋友,因為水平有限,如果有錯誤敬請指正,十分感激!