一 前言
React
高階組件(HOC
),對於很多react
開發者來說並不陌生,它是靈活使用react
組件的一種技巧,高階組件本身不是組件,它是一個參數為組件,返回值也是一個組件的函數。高階作用用於強化組件,復用邏輯,提升渲染性能等作用。高階組件也並不是很難理解,其實接觸過后還是蠻簡單的,接下來我將按照,高階組件理解?,高階組件具體怎么使用?應用場景, 高階組件實踐(源碼級別) 為突破口,帶大家詳細了解一下高階組件。本文篇幅比較長,建議收藏觀看
我們帶着問題去開始今天的討論:
-
1 什么是高階組件,它解決了什么問題?
-
2 有幾種高階組件,它們優缺點是什么?
-
3 如何寫一個優秀高階組件?
-
4
hoc
怎么處理靜態屬性,跨層級ref
等問題? -
5 高階組件怎么控制渲染,隔離渲染?
-
6 高階組件怎么監控原始組件的狀態?
-
...
高階組件(HOC)是 React 中用於復用組件邏輯的一種高級技巧。HOC 自身不是 React API 的一部分,它是一種基於 React 的組合特性而形成的設計模式。
二 全方位看高階組件
1 幾種包裝強化組件的方式
① mixin模式
原型圖
老版本的react-mixins
在react
初期提供一種組合方法。通過React.createClass
,加入mixins
屬性,具體用法和vue
中mixins
相似。具體實現如下。
const customMixin = { componentDidMount(){ console.log( '------componentDidMount------' ) }, say(){ console.log(this.state.name) } } const APP = React.createClass({ mixins: [ customMixin ], getInitialState(){ return { name:'alien' } }, render(){ const { name } = this.state return <div> hello ,world , my name is { name } </div> } })
這種mixins
只能存在createClass
中,后來React.createClass
連同mixins
這種模式被廢棄了。mixins
會帶來一些負面的影響。
-
1 mixin引入了隱式依賴關系。
-
2 不同mixins之間可能會有先后順序甚至代碼沖突覆蓋的問題
-
3 mixin代碼會導致滾雪球式的復雜性
衍生方式
createClass
的廢棄,不代表mixin
模式退出react
舞台,在有狀態組件class
,我們可以通過原型鏈繼承來實現mixins
。
const customMixin = { /* 自定義 mixins */
componentDidMount(){
console.log( '------componentDidMount------' )
},
say(){
console.log(this.state.name) }
} function componentClassMixins(Component,mixin){ /* 繼承 */
for(let key in mixin){
Component.prototype[key] = mixin[key]
}
} class Index extends React.Component{
constructor(){
super()
this.state={ name:'alien' }
}
render(){
return <div> hello,world <button onClick={ this.say.bind(this) } > to say </button> </div>
}
}
componentClassMixins(Index,customMixin)
②extends繼承模式
原型圖
在class
組件盛行之后,我們可以通過繼承的方式進一步的強化我們的組件。這種模式的好處在於,可以封裝基礎功能組件,然后根據需要去extends
我們的基礎組件,按需強化組件,但是值得注意的是,必須要對基礎組件有足夠的掌握,否則會造成一些列意想不到的情況發生。
class Base extends React.Component{ constructor(){ super() this.state={ name:'alien' } } say(){ console.log('base components') } render(){ return <div> hello,world <button onClick={ this.say.bind(this) } >點擊</button> </div> } } class Index extends Base{ componentDidMount(){ console.log( this.state.name ) } say(){ /* 會覆蓋基類中的 say */ console.log('extends components') } } export default Index
③HOC模式
原型圖
HOC
是我們本章主要的講的內容,具體用法,我們接下來會慢慢道來,我們先簡單嘗試一個HOC
。
function HOC(Component) { return class wrapComponent extends React.Component{ constructor(){ super() this.state={ name:'alien' } } render=()=><Component { ...this.props } { ...this.state } /> } } @HOC class Index extends React.Component{ say(){ const { name } = this.props console.log(name) } render(){ return <div> hello,world <button onClick={ this.say.bind(this) } >點擊</button> </div> } }
④自定義hooks模式
原型圖
hooks
的誕生,一大部分原因是解決無狀態組件沒有state
和邏輯難以復用問題。hooks
可以將一段邏輯封裝起來,做到開箱即用,我這里就不多講了,接下來會出react-hooks
原理的文章,完成react-hooks
三部曲。感興趣的同學可以看筆者的另外二篇文章,里面詳細介紹了react-hooks
復用代碼邏輯的原則和方案。
傳送門:
玩轉react-hooks,自定義hooks設計模式及其實戰
react-hooks如何使用?
2 高階組件產生初衷
組件是把prop
渲染成UI
,而高階組件是將組件轉換成另外一個組件,我們更應該注意的是,經過包裝后的組件,獲得了那些強化,節省多少邏輯,或是解決了原有組件的那些缺陷,這就是高階組件的意義。我們先來思考一下高階組件究竟解決了什么問題🤔🤔🤔?
① 復用邏輯:高階組件更像是一個加工react
組件的工廠,批量對原有組件進行加工,包裝處理。我們可以根據業務需求定制化專屬的HOC
,這樣可以解決復用邏輯。
② 強化props:這個是HOC
最常用的用法之一,高階組件返回的組件,可以劫持上一層傳過來的props
,然后混入新的props
,來增強組件的功能。代表作react-router
中的withRouter
。
③ 賦能組件:HOC
有一項獨特的特性,就是可以給被HOC
包裹的業務組件,提供一些拓展功能,比如說額外的生命周期,額外的事件,但是這種HOC
,可能需要和業務組件緊密結合。典型案例react-keepalive-router
中的 keepaliveLifeCycle
就是通過HOC
方式,給業務組件增加了額外的生命周期。
④ 控制渲染:劫持渲染是hoc
一個特性,在wrapComponent
包裝組件中,可以對原來的組件,進行條件渲染
,節流渲染
,懶加載
等功能,后面會詳細講解,典型代表做react-redux
中connect
和 dva
中 dynamic
組件懶加載。
我會針對高階組件的初衷展開,詳細介紹其原理已經用法。跟上我的思路,我們先來看一下,高階組件如何在我們的業務組件中使用的。
3 高階組件使用和編寫結構
HOC
使用指南是非常簡單的,只需要將我們的組件進行包裹就可以了。
使用:裝飾器模式和函數包裹模式
對於class
聲明的有狀態組件,我們可以用裝飾器模式,對類組件進行包裝:
@withStyles(styles) @withRouter @keepaliveLifeCycle class Index extends React.Componen{ /* ... */ }
我們要注意一下包裝順序,越靠近Index
組件的,就是越內層的HOC
,離組件Index
也就越近。
對於無狀態組件(函數聲明)我們可以這么寫:
function Index(){ /* .... */ } export default withStyles(styles)(withRouter( keepaliveLifeCycle(Index) ))
模型:嵌套HOC
對於不需要傳遞參數的HOC
,我們編寫模型我們只需要嵌套一層就可以,比如withRouter
,
function withRouter(){ return class wrapComponent extends React.Component{ /* 編寫邏輯 */ } }
對於需要參數的HOC
,我們需要一層代理,如下:
function connect (mapStateToProps){ /* 接受第一個參數 */ return function connectAdvance(wrapCompoent){ /* 接受組件 */ return class WrapComponent extends React.Component{ } } }
我們看出兩種hoc
模型很簡單,對於代理函數,可能有一層,可能有很多層,不過不要怕,無論多少層本質上都是一樣的,我們只需要一層一層剝離開,分析結構,整個hoc
結構和脈絡就會清晰可見。吃透hoc
也就易如反掌。
4 兩種不同的高階組件
常用的高階組件有兩種方式正向的屬性代理和反向的組件繼承,兩者之前有一些共性和區別。接下具體介紹兩者區別,在第三部分會詳細介紹具體實現。
正向屬性代理
所謂正向屬性代理,就是用組件包裹一層代理組件,在代理組件上,我們可以做一些,對源組件的代理操作。在fiber tree
上,先mounted
代理組件,然后才是我們的業務組件。我們可以理解為父子組件關系,父組件對子組件進行一系列強化操作。
function HOC(WrapComponent){ return class Advance extends React.Component{ state={ name:'alien' } render(){ return <WrapComponent { ...this.props } { ...this.state } /> } } }
優點
-
① 正常屬性代理可以和業務組件低耦合,零耦合,對於
條件渲染
和props屬性增強
,只負責控制子組件渲染和傳遞額外的props
就可以,所以無須知道,業務組件做了些什么。所以正向屬性代理,更適合做一些開源項目的hoc
,目前開源的HOC
基本都是通過這個模式實現的。 -
② 同樣適用於
class
聲明組件,和function
聲明的組件。 -
③ 可以完全隔離業務組件的渲染,相比反向繼承,屬性代理這種模式。可以完全控制業務組件渲染與否,可以避免
反向繼承
帶來一些副作用,比如生命周期的執行。 -
④ 可以嵌套使用,多個
hoc
是可以嵌套使用的,而且一般不會限制包裝HOC
的先后順序。
缺點
-
① 一般無法直接獲取業務組件的狀態,如果想要獲取,需要
ref
獲取組件實例。 -
② 無法直接繼承靜態屬性。如果需要繼承需要手動處理,或者引入第三方庫。
例子:
class Index extends React.Component{ render(){ return <div> hello,world </div> } } Index.say = function(){ console.log('my name is alien') } function HOC(Component) { return class wrapComponent extends React.Component{ render(){ return <Component { ...this.props } { ...this.state } /> } } } const newIndex = HOC(Index) console.log(newIndex.say)
打印結果
反向繼承
反向繼承和屬性代理有一定的區別,在於包裝后的組件繼承了業務組件本身,所以我們我無須在去實例化我們的業務組件。當前高階組件就是繼承后,加強型的業務組件。這種方式類似於組件的強化,所以你必要要知道當前
class Index extends React.Component{ render(){ return <div> hello,world </div> } } function HOC(Component){ return class wrapComponent extends Component{ /* 直接繼承需要包裝的組件 */ } } export default HOC(Index)
優點
-
① 方便獲取組件內部狀態,比如
state
,props
,生命周期,綁定的事件函數等 -
②
es6
繼承可以良好繼承靜態屬性。我們無須對靜態屬性和方法進行額外的處理。
class Index extends React.Component{ render(){ return <div> hello,world </div> } } Index.say = function(){ console.log('my name is alien') } function HOC(Component) { return class wrapComponent extends Component{ } } const newIndex = HOC(Index) console.log(newIndex.say)
打印結果
缺點
-
① 無狀態組件無法使用。
-
② 和被包裝的組件強耦合,需要知道被包裝的組件的內部狀態,具體是做什么?
-
③ 如果多個反向繼承
hoc
嵌套在一起,當前狀態會覆蓋上一個狀態。這樣帶來的隱患是非常大的,比如說有多個componentDidMount
,當前componentDidMount
會覆蓋上一個componentDidMount
。這樣副作用串聯起來,影響很大。
三 如何編寫高階組件
接下來我們來看看,如何編寫一個高階組件,你可以參考如下的情景,去編寫屬於自己的HOC
。
1 強化props
① 混入props
這個是高階組件最常用的功能,承接上層的props
,在混入自己的props
,來強化組件。
有狀態組件(屬性代理)
function classHOC(WrapComponent){ return class Idex extends React.Component{ state={ name:'alien' } componentDidMount(){ console.log('HOC') } render(){ return <WrapComponent { ...this.props } { ...this.state } /> } } } function Index(props){ const { name } = props useEffect(()=>{ console.log( 'index' ) },[]) return <div> hello,world , my name is { name } </div> } export default classHOC(Index)
有狀態組件(屬性代理)
同樣也適用與無狀態組件。
function functionHoc(WrapComponent){ return function Index(props){ const [ state , setState ] = useState({ name :'alien' }) return <WrapComponent { ...props } { ...state } /> } }
效果
② 抽離state控制更新
高階組件可以將HOC
的state
的配合起來,控制業務組件的更新。這種用法在react-redux
中connect
高階組件中用到過,用於處理來自redux
中state
更改,帶來的訂閱更新作用。
我們將上述代碼進行改造。
function classHOC(WrapComponent){ return class Idex extends React.Component{ constructor(){ super() this.state={ name:'alien' } } changeName(name){ this.setState({ name }) } render(){ return <WrapComponent { ...this.props } { ...this.state } changeName={this.changeName.bind(this) } /> } } } function Index(props){ const [ value ,setValue ] = useState(null) const { name ,changeName } = props return <div> <div> hello,world , my name is { name }</div> 改變name <input onChange={ (e)=> setValue(e.target.value) } /> <button onClick={ ()=> changeName(value) } >確定</button> </div> } export default classHOC(Index)
效果
2 控制渲染
控制渲染是高階組件的一個很重要的特性,上邊說到的兩種高階組件,都能完成對組件渲染的控制。具體實現還是有區別的,我們一起來探索一下。
2.1 條件渲染
① 基礎 :動態渲染
對於屬性代理的高階組件,雖然不能在內部操控渲染狀態,但是可以在外層控制當前組件是否渲染,這種情況應用於,權限隔離,懶加載 ,延時加載等場景。
實現一個動態掛載組件的HOC
function renderHOC(WrapComponent){ return class Index extends React.Component{ constructor(props){ super(props) this.state={ visible:true } } setVisible(){ this.setState({ visible:!this.state.visible }) } render(){ const { visible } = this.state return <div className="box" > <button onClick={ this.setVisible.bind(this) } > 掛載組件 </button> { visible ? <WrapComponent { ...this.props } setVisible={ this.setVisible.bind(this) } /> : <div className="icon" ><SyncOutlined spin className="theicon" /></div> } </div> } } } class Index extends React.Component{ render(){ const { setVisible } = this.props return <div className="box" > <p>hello,my name is alien</p> <img src='https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&fm=26&gp=0.jpg' /> <button onClick={() => setVisible()} > 卸載當前組件 </button> </div> } } export default renderHOC(Index)
效果:
② 進階 :分片渲染
是不是感覺不是很過癮,為了讓大家加強對HOC
條件渲染的理解,我再做一個分片渲染+懶加載功能。為了讓大家明白,我也是絞盡腦汁啊😂😂😂。
進階:實現一個懶加載功能的HOC,可以實現組件的分片渲染,用於分片渲染頁面,不至於一次渲染大量組件造成白屏效果
const renderQueue = [] let isFirstrender = false const tryRender = ()=>{ const render = renderQueue.shift() if(!render) return setTimeout(()=>{ render() /* 執行下一段渲染 */ },300) } /* HOC */ function renderHOC(WrapComponent){ return function Index(props){ const [ isRender , setRender ] = useState(false) useEffect(()=>{ renderQueue.push(()=>{ /* 放入待渲染隊列中 */ setRender(true) }) if(!isFirstrender) { tryRender() /**/ isFirstrender = true } },[]) return isRender ? <WrapComponent tryRender={tryRender} { ...props } /> : <div className='box' ><div className="icon" ><SyncOutlined spin /></div></div> } } /* 業務組件 */ class Index extends React.Component{ componentDidMount(){ const { name , tryRender} = this.props /* 上一部分渲染完畢,進行下一部分渲染 */ tryRender() console.log( name+'渲染') } render(){ return <div> <img src="https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&fm=26&gp=0.jpg" /> </div> } } /* 高階組件包裹 */ const Item = renderHOC(Index) export default () => { return <React.Fragment> <Item name="組件一" /> <Item name="組件二" /> <Item name="組件三" /> </React.Fragment> }
效果
大致流程,初始化的時候,HOC
中將渲染真正組件的渲染函數,放入renderQueue
隊列中,然后初始化渲染一次,接下來,每一個項目組件,完成 didMounted
狀態后,會從隊列中取出下一個渲染函數,渲染下一個組件, 一直到所有的渲染任務全部執行完畢,渲染隊列清空,有效的進行分片的渲染,這種方式對海量數據展示,很奏效。
用HOC
實現了條件渲染-分片渲染的功能,實際條件渲染理解起來很容易,就是通過變量,控制是否掛載組件,從而滿足項目本身需求,條件渲染可以演變成很多模式,我這里介紹了條件渲染的二種方式,希望大家能夠理解精髓所在。
③ 進階:異步組件(懶加載)
不知道大家有沒有用過dva
,里面的dynamic
就是應用HOC
模式實現的組件異步加載,我這里簡化了一下,提煉核心代碼,如下:
/* 路由懶加載HOC */ export default function AsyncRouter(loadRouter) { return class Content extends React.Component { state = {Component: null} componentDidMount() { if (this.state.Component) return loadRouter() .then(module => module.default) .then(Component => this.setState({Component}, )) } render() { const {Component} = this.state return Component ? <Component { ...this.props } /> : null } } }
使用
const Index = AsyncRouter(()=>import('../pages/index'))
hoc
還可以配合其他API
,做一下衍生的功能。如上配合import
實現異步加載功能。HOC
用起來非常靈活,
④ 反向繼承 :渲染劫持
HOC反向繼承模式,可以實現顆粒化的渲染劫持,也就是可以控制基類組件的render
函數,還可以篡改props,或者是children
,我們接下來看看,這種狀態下,怎么使用高階組件。
const HOC = (WrapComponent) => class Index extends WrapComponent { render() { if (this.props.visible) { return super.render() } else { return <div>暫無數據</div> } } }
⑤ 反向繼承:修改渲染樹
修改渲染狀態(劫持render替換子節點)
class Index extends React.Component{ render(){ return <div> <ul> <li>react</li> <li>vue</li> <li>Angular</li> </ul> </div> } } function HOC (Component){ return class Advance extends Component { render() { const element = super.render() const otherProps = { name:'alien' } /* 替換 Angular 元素節點 */ const appendElement = React.createElement('li' ,{} , `hello ,world , my name is ${ otherProps.name }` ) const newchild = React.Children.map(element.props.children.props.children,(child,index)=>{ if(index === 2) return appendElement return child }) return React.cloneElement(element, element.props, newchild) } } } export default HOC(Index)
效果
我們用劫持渲染的方式,來操縱super.render()
后的React.element
元素,然后配合 createElement
, cloneElement
, React.Children
等 api
,可以靈活操縱,真正的渲染react.element
,可以說是偷天換日,不亦樂乎。
2.2節流渲染
hoc
除了可以進行條件渲染,渲染劫持功能外,還可以進行節流渲染,也就是可以優化性能,具體怎么做,請跟上我的節奏往下看。
① 基礎: 節流原理
hoc
可以配合hooks
的useMemo
等API
配合使用,可以實現對業務組件的渲染控制,減少渲染次數,從而達到優化性能的效果。如下案例,我們期望當且僅當num
改變的時候,渲染組件,但是不影響接收的props
。我們應該這樣寫我們的HOC
。
function HOC (Component){ return function renderWrapComponent(props){ const { num } = props const RenderElement = useMemo(() => <Component {...props} /> ,[ num ]) return RenderElement } } class Index extends React.Component{ render(){ console.log(`當前組件是否渲染`,this.props) return <div>hello,world, my name is alien </div> } } const IndexHoc = HOC(Index) export default ()=> { const [ num ,setNumber ] = useState(0) const [ num1 ,setNumber1 ] = useState(0) const [ num2 ,setNumber2 ] = useState(0) return <div> <IndexHoc num={ num } num1={num1} num2={ num2 } /> <button onClick={() => setNumber(num + 1) } >num++</button> <button onClick={() => setNumber1(num1 + 1) } >num1++</button> <button onClick={() => setNumber2(num2 + 1) } >num2++</button> </div> }
效果:
如圖所示,當我們只有點擊 num++
時候,才重新渲染子組件,點擊其他按鈕,只是負責傳遞了props
,達到了期望的效果。
② 進階:定制化渲染流
思考:🤔上述的案例只是介紹了原理,在實際項目中,是量化生產不了的,原因是,我們需要針對不同props
變化,寫不同的HOC
組件,這樣根本起不了Hoc
真正的用途,也就是HOC
產生的初衷。所以我們需要對上述hoc
進行改造升級,是組件可以根據定制化方向,去渲染組件。也就是Hoc
生成的時候,已經按照某種契約去執行渲染。
function HOC (rule){ return function (Component){ return function renderWrapComponent(props){ const dep = rule(props) const RenderElement = useMemo(() => <Component {...props} /> ,[ dep ]) return RenderElement } } } /* 只有 props 中 num 變化 ,渲染組件 */ @HOC( (props)=> props['num']) class IndexHoc extends React.Component{ render(){ console.log(`組件一渲染`,this.props) return <div> 組件一 :hello,world </div> } } /* 只有 props 中 num1 變化 ,渲染組件 */ @HOC((props)=> props['num1']) class IndexHoc1 extends React.Component{ render(){ console.log(`組件二渲染`,this.props) return <div> 組件二 :my name is alien </div> } } export default ()=> { const [ num ,setNumber ] = useState(0) const [ num1 ,setNumber1 ] = useState(0) const [ num2 ,setNumber2 ] = useState(0) return <div> <IndexHoc num={ num } num1={num1} num2={ num2 } /> <IndexHoc1 num={ num } num1={num1} num2={ num2 } /> <button onClick={() => setNumber(num + 1) } >num++</button> <button onClick={() => setNumber1(num1 + 1) } >num1++</button> <button onClick={() => setNumber2(num2 + 1) } >num2++</button> </div> }
效果
完美實現了效果。這用高階組件模式,可以靈活控制React
組件層面上的,props
數據流和更新流,優秀的高階組件有 mobx
中observer
,inject
, react-redux
中的connect
,感興趣的同學,可以抽時間研究一下。
3 賦能組件
高階組件除了上述兩種功能之外,還可以賦能組件,比如加一些額外生命周期
,劫持事件,監控日志等等。
3.1 劫持原型鏈-劫持生命周期,事件函數
① 屬性代理實現
function HOC (Component){ const proDidMount = Component.prototype.componentDidMount Component.prototype.componentDidMount = function(){ console.log('劫持生命周期:componentDidMount') proDidMount.call(this) } return class wrapComponent extends React.Component{ render(){ return <Component {...this.props} /> } } } @HOC class Index extends React.Component{ componentDidMount(){ console.log('———didMounted———') } render(){ return <div>hello,world</div> } }
效果
② 反向繼承實現
反向繼承,因為在繼承原有組件的基礎上,可以對原有組件的生命周期或事件進行劫持,甚至是替換。
function HOC (Component){ const didMount = Component.prototype.componentDidMount return class wrapComponent extends Component{ componentDidMount(){ console.log('------劫持生命周期------') if (didMount) { didMount.apply(this) /* 注意 `this` 指向問題。*/ } } render(){ return super.render() } } } @HOC class Index extends React.Component{ componentDidMount(){ console.log('———didMounted———') } render(){ return <div>hello,world</div> } }
3.2 事件監控
HOC
還可以對原有組件進行監控。比如對一些事件監控
,錯誤監控
,事件監聽
等一系列操作。
① 組件內的事件監聽
接下來,我們做一個HOC
,只對組件內的點擊事件做一個監聽效果。
function ClickHoc (Component){ return function Wrap(props){ const dom = useRef(null) useEffect(()=>{ const handerClick = () => console.log('發生點擊事件') dom.current.addEventListener('click',handerClick) return () => dom.current.removeEventListener('click',handerClick) },[]) return <div ref={dom} ><Component {...props} /></div> } } @ClickHoc class Index extends React.Component{ render(){ return <div className='index' > <p>hello,world</p> <button>組件內部點擊</button> </div> } } export default ()=>{ return <div className='box' > <Index /> <button>組件外部點擊</button> </div> }
效果
3 ref助力操控組件實例
對於屬性代理我們雖然不能直接獲取組件內的狀態,但是我們可以通過ref
獲取組件實例,獲取到組件實例,就可以獲取組件的一些狀態,或是手動觸發一些事件,進一步強化組件,但是注意的是:class
聲明的有狀態組件才有實例,function
聲明的無狀態組件不存在實例。
① 屬性代理-添加額外生命周期
我們可以針對某一種情況, 給組件增加額外的生命周期,我做了一個簡單的demo
,監聽number
改變,如果number
改變,就自動觸發組件的監聽函數handerNumberChange
。具體寫法如下
function Hoc(Component){ return class WrapComponent extends React.Component{ constructor(){ super() this.node = null } UNSAFE_componentWillReceiveProps(nextprops){ if(nextprops.number !== this.props.number ){ this.node.handerNumberChange && this.node.handerNumberChange.call(this.node) } } render(){ return <Component {...this.props} ref={(node) => this.node = node } /> } } } @Hoc class Index extends React.Component{ handerNumberChange(){ /* 監聽 number 改變 */ } render(){ return <div>hello,world</div> } }
這種寫法有點不盡人意,大家不要着急,在第四部分,源碼實戰中,我會介紹一種更好的場景。方便大家理解Hoc
對原有組件的賦能。
4 總結
上面我分別按照hoc
主要功能,強化props , 控制渲染 ,賦能組件 三個方向對HOC
編寫做了一個詳細介紹,和應用場景的介紹,目的讓大家在理解高階組件的時候,更明白什么時候會用到?,怎么樣去寫?` 里面涵蓋的知識點我總一個總結。
對於屬性代理HOC,我們可以:
-
強化props & 抽離state。
-
條件渲染,控制渲染,分片渲染,懶加載。
-
劫持事件和生命周期
-
ref控制組件實例
-
添加事件監聽器,日志
對於反向代理的HOC,我們可以:
-
劫持渲染,操縱渲染樹
-
控制/替換生命周期,直接獲取組件狀態,綁定事件。
每個應用場景,我都舉了例子🌰🌰,大家可以結合例子深入了解一下其原理和用途。
四 高階組件源碼級實踐
hoc
的應用場景有很多,也有很多好的開源項目,供我們學習和參考,接下來我真對三個方向上的功能用途,分別從源碼角度解析HOC
的用途。
1 強化prop- withRoute
用過withRoute
的同學,都明白其用途,withRoute
用途就是,對於沒有被Route
包裹的組件,給添加history
對象等和路由相關的狀態,方便我們在任意組件中,都能夠獲取路由狀態,進行路由跳轉,這個HOC
目的很清楚,就是強化props
,把Router
相關的狀態都混入到props
中,我們看看具體怎么實現的。
function withRouter(Component) { const displayName = `withRouter(${Component.displayName || Component.name})`; const C = props => { /* 獲取 */ const { wrappedComponentRef, ...remainingProps } = props; return ( <RouterContext.Consumer> {context => { return ( <Component {...remainingProps} {...context} ref={wrappedComponentRef} /> ); }} </RouterContext.Consumer> ); }; C.displayName = displayName; C.WrappedComponent = Component; /* 繼承靜態屬性 */ return hoistStatics(C, Component); } export default withRouter
withRoute
的流程實際很簡單,就是先從props
分離出ref
和props
,然后從存放整個route
對象上下文RouterContext
取出route
對象,然后混入到原始組件的props
中,最后用hoistStatics
繼承靜態屬性。至於hoistStatics
我們稍后會講到。
2 控制渲染案例 connect
由於connect
源碼比較長和難以理解,所以我們提取精髓,精簡精簡再精簡, 總結的核心功能如下,connect
的作用也有合並props
,但是更重要的是接受state
,來控制更新組件。下面這個代碼中,為了方便大家理解,我都給簡化了。希望大家能夠理解hoc
如何派發和控制更新流的。
import store from './redux/store' import { ReactReduxContext } from './Context' import { useContext } from 'react' function connect(mapStateToProps){ /* 第一層:接收訂閱state函數 */ return function wrapWithConnect (WrappedComponent){ /* 第二層:接收原始組件 */ function ConnectFunction(props){ const [ , forceUpdate ] = useState(0) const { reactReduxForwardedRef ,...wrapperProps } = props /* 取出Context */ const { store } = useContext(ReactReduxContext) /* 強化props:合並 store state 和 props */ const trueComponentProps = useMemo(()=>{ /* 只有props或者訂閱的state變化,才返回合並后的props */ return selectorFactory(mapStateToProps(store.getState()),wrapperProps) },[ store , wrapperProps ]) /* 只有 trueComponentProps 改變時候,更新組件。*/ const renderedWrappedComponent = useMemo( () => ( <WrappedComponent {...trueComponentProps} ref={reactReduxForwardedRef} /> ), [reactReduxForwardedRef, WrappedComponent, trueComponentProps] ) useEffect(()=>{ /* 訂閱更新 */ const checkUpdate = () => forceUpdate(new Date().getTime()) store.subscribe( checkUpdate ) },[ store ]) return renderedWrappedComponent } /* React.memo 包裹 */ const Connect = React.memo(ConnectFunction) /* 處理hoc,獲取ref問題 */ if(forwardRef){ const forwarded = React.forwardRef(function forwardConnectRef( props,ref) { return <Connect {...props} reactReduxForwardedRef={ref} reactReduxForwardedRef={ref} /> }) return hoistStatics(forwarded, WrappedComponent) } /* 繼承靜態屬性 */ return hoistStatics(Connect,WrappedComponent) } } export default Index
connect
涉及到的功能點還真不少呢,首先第一層接受訂閱函數,第二層接收原始組件,然后用forwardRef
處理ref
,用hoistStatics
處理靜態屬性的繼承,在包裝組件內部,合並props
,useMemo
緩存原始組件,只有合並后的props
發生變化,才更新組件,然后在useEffect
內部通過store.subscribe()
訂閱更新。這里省略了Subscription
概念,真正的connect
中有一個Subscription
專門負責訂閱消息。
3 賦能組件-緩存生命周期 keepaliveLifeCycle
之前筆者寫了一個react
緩存頁面的開源庫react-keepalive-router
,可以實現vue
中 keepalive
+ router
功能,最初的版本沒有緩存周期的,但是后來熱心讀者,期望在被緩存的路由組件中加入緩存周期,類似activated
這種的,后來經過我的分析打算用HOC
來實現此功能。
於是乎 react-keepalive-router
加入了全新的頁面組件生命周期 actived
和 unActived
, actived
作為緩存路由組件激活時候用,初始化的時候會默認執行一次 , unActived
作為路由組件緩存完成后調用。但是生命周期需要用一個 HOC
組件keepaliveLifeCycle
包裹。
使用
import React from 'react' import { keepaliveLifeCycle } from 'react-keepalive-router' @keepaliveLifeCycle class index extends React.Component<any,any>{ state={ activedNumber:0, unActivedNumber:0 } actived(){ this.setState({ activedNumber:this.state.activedNumber + 1 }) } unActived(){ this.setState({ unActivedNumber:this.state.unActivedNumber + 1 }) } render(){ const { activedNumber , unActivedNumber } = this.state return <div style={{ marginTop :'50px' }} > <div> 頁面 actived 次數:{activedNumber} </div> <div> 頁面 unActived 次數:{unActivedNumber} </div> </div> } } export default index
原理
import {lifeCycles} from '../core/keeper' import hoistNonReactStatic from 'hoist-non-react-statics' function keepaliveLifeCycle(Component) { class Hoc extends React.Component { cur = null handerLifeCycle = type => { if (!this.cur) return const lifeCycleFunc = this.cur[type] isFuntion(lifeCycleFunc) && lifeCycleFunc.call(this.cur) } componentDidMount() { const {cacheId} = this.props cacheId && (lifeCycles[cacheId] = this.handerLifeCycle) } componentWillUnmount() { const {cacheId} = this.props delete lifeCycles[cacheId] } render=() => <Component {...this.props} ref={cur => (this.cur = cur)}/> } return hoistNonReactStatic(Hoc,Component) }
keepaliveLifeCycle
的原理很簡單,就是通過ref
或獲取 class
組件的實例,在 hoc
初始化時候進行生命周期的綁定, 在 hoc
銷毀階段,對生命周期進行解綁, 然后交給keeper
統一調度,keeper
通過調用實例下面的生命周期函數,來實現緩存生命周期功能的。
五 高階組件的注意事項
1 謹慎修改原型鏈
function HOC (Component){ const proDidMount = Component.prototype.componentDidMount Component.prototype.componentDidMount = function(){ console.log('劫持生命周期:componentDidMount') proDidMount.call(this) } return Component }
這樣做會產生一些不良后果。比如如果你再用另一個同樣會修改 componentDidMount
的 HOC
增強它,那么前面的 HOC
就會失效!同時,這個 HOC
也無法應用於沒有生命周期的函數組件。
2 繼承靜態屬性
在用屬性代理的方式編寫HOC
的時候,要注意的是就是,靜態屬性丟失的問題,前面提到了,如果不做處理,靜態方法就會全部丟失。
手動繼承
我們可以手動將原始組件的靜態方法copy
到 hoc
組件上來,但前提是必須准確知道應該拷貝哪些方法。
function HOC(Component) { class WrappedComponent extends React.Component { /*...*/ } // 必須准確知道應該拷貝哪些方法 WrappedComponent.staticMethod = Component.staticMethod return WrappedComponent }
引入第三方庫
這樣每個靜態方法都綁定會很累,尤其對於開源的hoc
,對原生組件的靜態方法是未知的,我們可以使用 hoist-non-react-statics
自動拷貝所有的靜態方法:
import hoistNonReactStatic from 'hoist-non-react-statics' function HOC(Component) { class WrappedComponent extends React.Component { /*...*/ } hoistNonReactStatic(WrappedComponent,Component) return WrappedComponent }
3 跨層級捕獲ref
高階組件的約定是將所有 props
傳遞給被包裝組件,但這對於 refs
並不適用。那是因為 ref
實際上並不是一個 prop
- 就像 key
一樣,它是由 React
專門處理的。如果將 ref
添加到 HOC
的返回組件中,則 ref
引用指向容器組件,而不是被包裝組件。我們可以通過forwardRef
來解決這個問題。
/** * * @param {*} Component 原始組件 * @param {*} isRef 是否開啟ref模式 */ function HOC(Component,isRef){ class Wrap extends React.Component{ render(){ const { forwardedRef ,...otherprops } = this.props return <Component ref={forwardedRef} {...otherprops} /> } } if(isRef){ return React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> ) } return Wrap } class Index extends React.Component{ componentDidMount(){ console.log(666) } render(){ return <div>hello,world</div> } } const HocIndex = HOC(Index,true) export default ()=>{ const node = useRef(null) useEffect(()=>{ /* 就可以跨層級,捕獲到 Index 組件的實例了 */ console.log(node.current.componentDidMount) },[]) return <div><HocIndex ref={node} /></div> }
打印結果:
如上就解決了,HOC
跨層級捕獲ref
的問題。
4 render中不要聲明HOC
🙅錯誤寫法:
class Index extends React.Component{ render(){ const WrapHome = HOC(Home) return <WrapHome /> } }
如果這么寫,會造成一個極大的問題,因為每一次HOC
都會返回一個新的WrapHome
,react diff
會判定兩次不是同一個組件,那么每次Index
組件 render
觸發,WrapHome
,會重新掛載,狀態會全都丟失。如果想要動態綁定HOC
,請參考如下方式。
🙆正確寫法:
const WrapHome = HOC(Home) class index extends React.Component{ render(){ return <WrapHome /> } }
六 總結
本文從高階組件功能為切入點,介紹二種不同的高階組件如何編寫,應用場景,以及實踐。涵蓋了大部分耳熟能詳的開源高階組件的應用場景,如果你覺得這篇文章對你有啟發,最好還是按照文章中的demo
,跟着敲一遍,加深印象,知道什么場景用高階組件,怎么用高階組件。
實踐是檢驗真理的唯一標准
,希望大家能把高階組件碼
起來,用起來。
喜歡這篇文章?歡迎打賞~~