<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          「react進(jìn)階」一文吃透React高階組件(HOC)

          共 27923字,需瀏覽 56分鐘

           ·

          2021-03-24 17:38

          一 前言

          React高階組件(HOC),對于很多react開發(fā)者來說并不陌生,它是靈活使用react組件的一種技巧,高階組件本身不是組件,它是一個參數(shù)為組件,返回值也是一個組件的函數(shù)。高階作用用于強(qiáng)化組件,復(fù)用邏輯,提升渲染性能等作用。高階組件也并不是很難理解,其實接觸過后還是蠻簡單的,接下來我將按照,高階組件理解?,高階組件具體怎么使用?應(yīng)用場景, 高階組件實踐(源碼級別) 為突破口,帶大家詳細(xì)了解一下高階組件。本文篇幅比較長,建議收藏觀看

          我們帶著問題去開始今天的討論:

          • 1 什么是高階組件,它解決了什么問題?

          • 2 有幾種高階組件,它們優(yōu)缺點(diǎn)是什么?

          • 3 如何寫一個優(yōu)秀高階組件?

          • hoc怎么處理靜態(tài)屬性,跨層級ref等問題?

          • 5 高階組件怎么控制渲染,隔離渲染?

          • 6 高階組件怎么監(jiān)控原始組件的狀態(tài)?

          • ...

          高階組件(HOC)是 React 中用于復(fù)用組件邏輯的一種高級技巧。HOC 自身不是 React API 的一部分,它是一種基于 React 的組合特性而形成的設(shè)計模式。

          二 全方位看高階組件

          1 幾種包裝強(qiáng)化組件的方式

          ① mixin模式

          原型圖

          老版本的react-mixins

          react初期提供一種組合方法。通過React.createClass,加入mixins屬性,具體用法和vue 中mixins相似。具體實現(xiàn)如下。

          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會帶來一些負(fù)面的影響。

          • 1 mixin引入了隱式依賴關(guān)系。

          • 2 不同mixins之間可能會有先后順序甚至代碼沖突覆蓋的問題

          • 3 mixin代碼會導(dǎo)致滾雪球式的復(fù)雜性

          衍生方式

          createClass的廢棄,不代表mixin模式退出react舞臺,在有狀態(tài)組件class,我們可以通過原型鏈繼承來實現(xiàn)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組件盛行之后,我們可以通過繼承的方式進(jìn)一步的強(qiáng)化我們的組件。這種模式的好處在于,可以封裝基礎(chǔ)功能組件,然后根據(jù)需要去extends我們的基礎(chǔ)組件,按需強(qiáng)化組件,但是值得注意的是,必須要對基礎(chǔ)組件有足夠的掌握,否則會造成一些列意想不到的情況發(fā)生。

          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) } >點(diǎn)擊</button>  </div>  }}class Index extends Base{  componentDidMount(){    console.log( this.state.name )  }  say(){ /* 會覆蓋基類中的 say  */    console.log('extends components')  }}export default Index

          ③HOC模式

          原型圖

          HOC是我們本章主要的講的內(nèi)容,具體用法,我們接下來會慢慢道來,我們先簡單嘗試一個HOC。

          function HOC(Component) {  return class wrapComponent extends React.Component{     constructor(){       super()       this.state={         name:'alien'       }     }     render=()=><Component { ...this.props } { ...this.state } />  }}
          @HOCclass Index extends React.Component{ say(){ const { name } = this.props console.log(name) } render(){ return <div> hello,world <button onClick={ this.say.bind(this) } >點(diǎn)擊</button> </div> }}

          ④自定義hooks模式

          原型圖

          hooks的誕生,一大部分原因是解決無狀態(tài)組件沒有state邏輯難以復(fù)用問題。hooks可以將一段邏輯封裝起來,做到開箱即用,我這里就不多講了,接下來會出react-hooks原理的文章,完成react-hooks三部曲。感興趣的同學(xué)可以看筆者的另外二篇文章,里面詳細(xì)介紹了react-hooks復(fù)用代碼邏輯的原則和方案。

          傳送門:

          玩轉(zhuǎn)react-hooks,自定義hooks設(shè)計模式及其實戰(zhàn)

          react-hooks如何使用?

          2 高階組件產(chǎn)生初衷

          組件是把prop渲染成UI,而高階組件是將組件轉(zhuǎn)換成另外一個組件,我們更應(yīng)該注意的是,經(jīng)過包裝后的組件,獲得了那些強(qiáng)化,節(jié)省多少邏輯,或是解決了原有組件的那些缺陷,這就是高階組件的意義。我們先來思考一下高階組件究竟解決了什么問題???????

          ① 復(fù)用邏輯:高階組件更像是一個加工react組件的工廠,批量對原有組件進(jìn)行加工,包裝處理。我們可以根據(jù)業(yè)務(wù)需求定制化專屬的HOC,這樣可以解決復(fù)用邏輯。

          ② 強(qiáng)化props:這個是HOC最常用的用法之一,高階組件返回的組件,可以劫持上一層傳過來的props,然后混入新的props,來增強(qiáng)組件的功能。代表作react-router中的withRouter。

          ③ 賦能組件HOC有一項獨(dú)特的特性,就是可以給被HOC包裹的業(yè)務(wù)組件,提供一些拓展功能,比如說額外的生命周期,額外的事件,但是這種HOC,可能需要和業(yè)務(wù)組件緊密結(jié)合。典型案例react-keepalive-router中的 keepaliveLifeCycle就是通過HOC方式,給業(yè)務(wù)組件增加了額外的生命周期。

          ④ 控制渲染:劫持渲染是hoc一個特性,在wrapComponent包裝組件中,可以對原來的組件,進(jìn)行條件渲染,節(jié)流渲染,懶加載等功能,后面會詳細(xì)講解,典型代表做react-reduxconnect和 dva中 dynamic 組件懶加載。

          我會針對高階組件的初衷展開,詳細(xì)介紹其原理已經(jīng)用法。跟上我的思路,我們先來看一下,高階組件如何在我們的業(yè)務(wù)組件中使用的。

          3 高階組件使用和編寫結(jié)構(gòu)

          HOC使用指南是非常簡單的,只需要將我們的組件進(jìn)行包裹就可以了。

          使用:裝飾器模式和函數(shù)包裹模式

          對于class聲明的有狀態(tài)組件,我們可以用裝飾器模式,對類組件進(jìn)行包裝:

          @withStyles(styles)@withRouter@keepaliveLifeCycleclass Index extends React.Componen{    /* ... */}

          我們要注意一下包裝順序,越靠近Index組件的,就是越內(nèi)層的HOC,離組件Index也就越近。

          對于無狀態(tài)組件(函數(shù)聲明)我們可以這么寫:

          function Index(){    /* .... */}export default withStyles(styles)(withRouter( keepaliveLifeCycle(Index) )) 

          模型:嵌套HOC

          對于不需要傳遞參數(shù)的HOC,我們編寫模型我們只需要嵌套一層就可以,比如withRouter,

          function withRouter(){    return class wrapComponent extends React.Component{        /* 編寫邏輯 */    }}

          對于需要參數(shù)的HOC,我們需要一層代理,如下:

          function connect (mapStateToProps){    /* 接受第一個參數(shù) */    return function connectAdvance(wrapCompoent){        /* 接受組件 */        return class WrapComponent extends React.Component{  }    }}

          我們看出兩種hoc模型很簡單,對于代理函數(shù),可能有一層,可能有很多層,不過不要怕,無論多少層本質(zhì)上都是一樣的,我們只需要一層一層剝離開,分析結(jié)構(gòu),整個hoc結(jié)構(gòu)和脈絡(luò)就會清晰可見。吃透hoc也就易如反掌。

          4 兩種不同的高階組件

          常用的高階組件有兩種方式正向的屬性代理反向的組件繼承,兩者之前有一些共性和區(qū)別。接下具體介紹兩者區(qū)別,在第三部分會詳細(xì)介紹具體實現(xiàn)。

          正向?qū)傩源?/h3>

          所謂正向?qū)傩源?,就是用組件包裹一層代理組件,在代理組件上,我們可以做一些,對源組件的代理操作。在fiber tree 上,先mounted代理組件,然后才是我們的業(yè)務(wù)組件。我們可以理解為父子組件關(guān)系,父組件對子組件進(jìn)行一系列強(qiáng)化操作。

          function HOC(WrapComponent){    return class Advance extends React.Component{       state={           name:'alien'       }       render(){           return <WrapComponent  { ...this.props } { ...this.state }  />       }    }}

          優(yōu)點(diǎn)

          • ① 正常屬性代理可以和業(yè)務(wù)組件低耦合,零耦合,對于條件渲染props屬性增強(qiáng),只負(fù)責(zé)控制子組件渲染和傳遞額外的props就可以,所以無須知道,業(yè)務(wù)組件做了些什么。所以正向?qū)傩源恚m合做一些開源項目的hoc,目前開源的HOC基本都是通過這個模式實現(xiàn)的。

          • ② 同樣適用于class聲明組件,和function聲明的組件。

          • ③ 可以完全隔離業(yè)務(wù)組件的渲染,相比反向繼承,屬性代理這種模式。可以完全控制業(yè)務(wù)組件渲染與否,可以避免反向繼承帶來一些副作用,比如生命周期的執(zhí)行。

          • ④ 可以嵌套使用,多個hoc是可以嵌套使用的,而且一般不會限制包裝HOC的先后順序。

          缺點(diǎn)

          • ① 一般無法直接獲取業(yè)務(wù)組件的狀態(tài),如果想要獲取,需要ref獲取組件實例。

          • ② 無法直接繼承靜態(tài)屬性。如果需要繼承需要手動處理,或者引入第三方庫。

          例子:

          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)

          打印結(jié)果

          反向繼承

          反向繼承和屬性代理有一定的區(qū)別,在于包裝后的組件繼承了業(yè)務(wù)組件本身,所以我們我無須在去實例化我們的業(yè)務(wù)組件。當(dāng)前高階組件就是繼承后,加強(qiáng)型的業(yè)務(wù)組件。這種方式類似于組件的強(qiáng)化,所以你必要要知道當(dāng)前

          class Index extends React.Component{  render(){    return <div> hello,world  </div>  }}function HOC(Component){    return class wrapComponent extends Component{ /* 直接繼承需要包裝的組件 */
          }}export default HOC(Index)

          優(yōu)點(diǎn)

          • ① 方便獲取組件內(nèi)部狀態(tài),比如state,props ,生命周期,綁定的事件函數(shù)等

          • ② es6繼承可以良好繼承靜態(tài)屬性。我們無須對靜態(tài)屬性和方法進(jìn)行額外的處理。



            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)

          打印結(jié)果

          缺點(diǎn)

          • ① 無狀態(tài)組件無法使用。

          • ② 和被包裝的組件強(qiáng)耦合,需要知道被包裝的組件的內(nèi)部狀態(tài),具體是做什么?

          • ③ 如果多個反向繼承hoc嵌套在一起,當(dāng)前狀態(tài)會覆蓋上一個狀態(tài)。這樣帶來的隱患是非常大的,比如說有多個componentDidMount,當(dāng)前componentDidMount會覆蓋上一個componentDidMount。這樣副作用串聯(lián)起來,影響很大。

          三 如何編寫高階組件

          接下來我們來看看,如何編寫一個高階組件,你可以參考如下的情景,去編寫屬于自己的HOC。

          1 強(qiáng)化props

          ① 混入props

          這個是高階組件最常用的功能,承接上層的props,在混入自己的props,來強(qiáng)化組件。

          有狀態(tài)組件(屬性代理)

          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)


          有狀態(tài)組件(屬性代理)

          同樣也適用與無狀態(tài)組件。

          function functionHoc(WrapComponent){    return function Index(props){        const [ state , setState ] = useState({ name :'alien'  })               return  <WrapComponent { ...props }  { ...state }   />    }}

          效果

          ② 抽離state控制更新

          高階組件可以將HOCstate的配合起來,控制業(yè)務(wù)組件的更新。這種用法在react-reduxconnect高階組件中用到過,用于處理來自reduxstate更改,帶來的訂閱更新作用。

          我們將上述代碼進(jìn)行改造。

          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 控制渲染

          控制渲染是高階組件的一個很重要的特性,上邊說到的兩種高階組件,都能完成對組件渲染的控制。具體實現(xiàn)還是有區(qū)別的,我們一起來探索一下。

          2.1 條件渲染

          ① 基礎(chǔ) :動態(tài)渲染

          對于屬性代理的高階組件,雖然不能在內(nèi)部操控渲染狀態(tài),但是可以在外層控制當(dāng)前組件是否渲染,這種情況應(yīng)用于,權(quán)限隔離,懶加載 ,延時加載等場景。

          實現(xiàn)一個動態(tài)掛載組件的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()} > 卸載當(dāng)前組件 </button> </div> }}export default renderHOC(Index)


          效果:

          ② 進(jìn)階 :分片渲染

          是不是感覺不是很過癮,為了讓大家加強(qiáng)對HOC條件渲染的理解,我再做一個分片渲染+懶加載功能。為了讓大家明白,我也是絞盡腦汁啊??????。

          進(jìn)階:實現(xiàn)一個懶加載功能的HOC,可以實現(xiàn)組件的分片渲染,用于分片渲染頁面,不至于一次渲染大量組件造成白屏效果

          const renderQueue = []let isFirstrender = false
          const tryRender = ()=>{ const render = renderQueue.shift() if(!render) return setTimeout(()=>{ render() /* 執(zhí)行下一段渲染 */ },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> }}/* 業(yè)務(wù)組件 */class Index extends React.Component{ componentDidMount(){ const { name , tryRender} = this.props /* 上一部分渲染完畢,進(jìn)行下一部分渲染 */ tryRender() console.log( name+'渲染') } render(){ return <div> <img src="https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&amp;fm=26&amp;gp=0.jpg" /> </div> }}/* 高階組件包裹 */const Item = renderHOC(Index)
          export default () => { return <React.Fragment> <Item name="組件一" /> <Item name="組件二" /> <Item name="組件三" /> </React.Fragment>}


          效果

          大致流程,初始化的時候,HOC中將渲染真正組件的渲染函數(shù),放入renderQueue隊列中,然后初始化渲染一次,接下來,每一個項目組件,完成 didMounted 狀態(tài)后,會從隊列中取出下一個渲染函數(shù),渲染下一個組件, 一直到所有的渲染任務(wù)全部執(zhí)行完畢,渲染隊列清空,有效的進(jìn)行分片的渲染,這種方式對海量數(shù)據(jù)展示,很奏效。

          HOC實現(xiàn)了條件渲染-分片渲染的功能,實際條件渲染理解起來很容易,就是通過變量,控制是否掛載組件,從而滿足項目本身需求,條件渲染可以演變成很多模式,我這里介紹了條件渲染的二種方式,希望大家能夠理解精髓所在。

          ③ 進(jìn)階:異步組件(懶加載)

          不知道大家有沒有用過dva,里面的dynamic就是應(yīng)用HOC模式實現(xiàn)的組件異步加載,我這里簡化了一下,提煉核心代碼,如下:

          /* 路由懶加載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實現(xiàn)異步加載功能。HOC用起來非常靈活,

          ④ 反向繼承 :渲染劫持

          HOC反向繼承模式,可以實現(xiàn)顆?;匿秩窘俪?,也就是可以控制基類組件的render函數(shù),還可以篡改props,或者是children,我們接下來看看,這種狀態(tài)下,怎么使用高階組件。

          const HOC = (WrapComponent) =>  class Index  extends WrapComponent {    render() {      if (this.props.visible) {        return super.render()      } else {        return <div>暫無數(shù)據(jù)</div>      }    }  }


          ⑤ 反向繼承:修改渲染樹

          修改渲染狀態(tài)(劫持render替換子節(jié)點(diǎn))

          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 元素節(jié)點(diǎn) */ 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節(jié)流渲染

          hoc除了可以進(jìn)行條件渲染,渲染劫持功能外,還可以進(jìn)行節(jié)流渲染,也就是可以優(yōu)化性能,具體怎么做,請跟上我的節(jié)奏往下看。

          ① 基礎(chǔ): 節(jié)流原理

          hoc可以配合hooksuseMemoAPI配合使用,可以實現(xiàn)對業(yè)務(wù)組件的渲染控制,減少渲染次數(shù),從而達(dá)到優(yōu)化性能的效果。如下案例,我們期望當(dāng)且僅當(dāng)num改變的時候,渲染組件,但是不影響接收的props。我們應(yīng)該這樣寫我們的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(`當(dāng)前組件是否渲染`,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>}

          效果:

          如圖所示,當(dāng)我們只有點(diǎn)擊 num++時候,才重新渲染子組件,點(diǎn)擊其他按鈕,只是負(fù)責(zé)傳遞了props,達(dá)到了期望的效果。

          ② 進(jìn)階:定制化渲染流

          思考:??上述的案例只是介紹了原理,在實際項目中,是量化生產(chǎn)不了的,原因是,我們需要針對不同props變化,寫不同的HOC組件,這樣根本起不了Hoc真正的用途,也就是HOC產(chǎn)生的初衷。所以我們需要對上述hoc進(jìn)行改造升級,是組件可以根據(jù)定制化方向,去渲染組件。也就是Hoc生成的時候,已經(jīng)按照某種契約去執(zhí)行渲染。

          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>}

          效果

          完美實現(xiàn)了效果。這用高階組件模式,可以靈活控制React組件層面上的,props數(shù)據(jù)流更新流,優(yōu)秀的高階組件有 mobx 中observer ,inject , react-redux中的connect,感興趣的同學(xué),可以抽時間研究一下。

          3 賦能組件

          高階組件除了上述兩種功能之外,還可以賦能組件,比如加一些額外生命周期,劫持事件,監(jiān)控日志等等。

          3.1 劫持原型鏈-劫持生命周期,事件函數(shù)

          ① 屬性代理實現(xiàn)

          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}  />      }  }}@HOCclass Index extends React.Component{   componentDidMount(){     console.log('———didMounted———')   }   render(){     return <div>hello,world</div>   }}

          效果

          ② 反向繼承實現(xiàn)

          反向繼承,因為在繼承原有組件的基礎(chǔ)上,可以對原有組件的生命周期事件進(jìn)行劫持,甚至是替換。

          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()      }  }}
          @HOCclass Index extends React.Component{ componentDidMount(){ console.log('———didMounted———') } render(){ return <div>hello,world</div> }}

          3.2 事件監(jiān)控

          HOC還可以對原有組件進(jìn)行監(jiān)控。比如對一些事件監(jiān)控,錯誤監(jiān)控,事件監(jiān)聽等一系列操作。

          ① 組件內(nèi)的事件監(jiān)聽

          接下來,我們做一個HOC,只對組件內(nèi)的點(diǎn)擊事件做一個監(jiān)聽效果。

          function ClickHoc (Component){  return  function Wrap(props){    const dom = useRef(null)    useEffect(()=>{     const handerClick = () => console.log('發(fā)生點(diǎn)擊事件')      dom.current.addEventListener('click',handerClick)     return () => dom.current.removeEventListener('click',handerClick)    },[])    return  <div ref={dom}  ><Component  {...props} /></div>  }}
          @ClickHocclass Index extends React.Component{ render(){ return <div className='index' > <p>hello,world</p> <button>組件內(nèi)部點(diǎn)擊</button> </div> }}export default ()=>{ return <div className='box' > <Index /> <button>組件外部點(diǎn)擊</button> </div>}

          效果

          3 ref助力操控組件實例

          對于屬性代理我們雖然不能直接獲取組件內(nèi)的狀態(tài),但是我們可以通過ref獲取組件實例,獲取到組件實例,就可以獲取組件的一些狀態(tài),或是手動觸發(fā)一些事件,進(jìn)一步強(qiáng)化組件,但是注意的是:class聲明的有狀態(tài)組件才有實例,function聲明的無狀態(tài)組件不存在實例。

          ① 屬性代理-添加額外生命周期

          我們可以針對某一種情況, 給組件增加額外的生命周期,我做了一個簡單的demo,監(jiān)聽number改變,如果number改變,就自動觸發(fā)組件的監(jiān)聽函數(shù)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 }  />      }  }}@Hocclass Index extends React.Component{  handerNumberChange(){      /* 監(jiān)聽 number 改變 */  }  render(){    return <div>hello,world</div>  }}

          這種寫法有點(diǎn)不盡人意,大家不要著急,在第四部分,源碼實戰(zhàn)中,我會介紹一種更好的場景。方便大家理解Hoc對原有組件的賦能。

          4 總結(jié)

          上面我分別按照hoc主要功能,強(qiáng)化props , 控制渲染 ,賦能組件 三個方向?qū)?code style="box-sizing: border-box;font-family: Menlo, Monaco, Consolas, "Courier New", monospace;font-size: 0.87em;word-break: break-word;border-radius: 2px;overflow-x: auto;background-color: rgb(255, 245, 245);color: rgb(255, 80, 44);padding: 0.065em 0.4em;">HOC編寫做了一個詳細(xì)介紹,和應(yīng)用場景的介紹,目的讓大家在理解高階組件的時候,更明白什么時候會用到?,怎么樣去寫?` 里面涵蓋的知識點(diǎn)我總一個總結(jié)。

          對于屬性代理HOC,我們可以:

          • 強(qiáng)化props & 抽離state。

          • 條件渲染,控制渲染,分片渲染,懶加載。

          • 劫持事件和生命周期

          • ref控制組件實例

          • 添加事件監(jiān)聽器,日志

          對于反向代理的HOC,我們可以:

          • 劫持渲染,操縱渲染樹

          • 控制/替換生命周期,直接獲取組件狀態(tài),綁定事件。

          每個應(yīng)用場景,我都舉了例子????,大家可以結(jié)合例子深入了解一下其原理和用途。

          四 高階組件源碼級實踐

          hoc的應(yīng)用場景有很多,也有很多好的開源項目,供我們學(xué)習(xí)和參考,接下來我真對三個方向上的功能用途,分別從源碼角度解析HOC的用途。

          1 強(qiáng)化prop- withRoute

          用過withRoute的同學(xué),都明白其用途,withRoute用途就是,對于沒有被Route包裹的組件,給添加history對象等和路由相關(guān)的狀態(tài),方便我們在任意組件中,都能夠獲取路由狀態(tài),進(jìn)行路由跳轉(zhuǎn),這個HOC目的很清楚,就是強(qiáng)化props,把Router相關(guān)的狀態(tài)都混入到props中,我們看看具體怎么實現(xiàn)的。

          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; /* 繼承靜態(tài)屬性 */ return hoistStatics(C, Component);}
          export default withRouter

          withRoute的流程實際很簡單,就是先從props分離出refprops,然后從存放整個route對象上下文RouterContext取出route對象,然后混入到原始組件的props中,最后用hoistStatics繼承靜態(tài)屬性。至于hoistStatics我們稍后會講到。

          2 控制渲染案例 connect

          由于connect源碼比較長和難以理解,所以我們提取精髓,精簡精簡再精簡, 總結(jié)的核心功能如下,connect的作用也有合并props,但是更重要的是接受state,來控制更新組件。下面這個代碼中,為了方便大家理解,我都給簡化了。希望大家能夠理解hoc如何派發(fā)控制更新流的。

          import store from './redux/store'import { ReactReduxContext } from './Context'import { useContext } from 'react'function connect(mapStateToProps){   /* 第一層:接收訂閱state函數(shù) */    return function wrapWithConnect (WrappedComponent){        /* 第二層:接收原始組件 */        function ConnectFunction(props){            const [ , forceUpdate ] = useState(0)            const { reactReduxForwardedRef ,...wrapperProps } = props                        /* 取出Context */            const { store } = useContext(ReactReduxContext)
          /* 強(qiáng)化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) } /* 繼承靜態(tài)屬性 */ return hoistStatics(Connect,WrappedComponent) } }export default Index

          connect 涉及到的功能點(diǎn)還真不少呢,首先第一層接受訂閱函數(shù),第二層接收原始組件,然后用forwardRef處理ref,用hoistStatics 處理靜態(tài)屬性的繼承,在包裝組件內(nèi)部,合并props,useMemo緩存原始組件,只有合并后的props發(fā)生變化,才更新組件,然后在useEffect內(nèi)部通過store.subscribe()訂閱更新。這里省略了Subscription概念,真正的connect中有一個Subscription專門負(fù)責(zé)訂閱消息。

          3 賦能組件-緩存生命周期 keepaliveLifeCycle

          之前筆者寫了一個react緩存頁面的開源庫react-keepalive-router,可以實現(xiàn)vue中 keepalive + router功能,最初的版本沒有緩存周期的,但是后來熱心讀者,期望在被緩存的路由組件中加入緩存周期,類似activated這種的,后來經(jīng)過我的分析打算用HOC來實現(xiàn)此功能。

          于是乎 react-keepalive-router加入了全新的頁面組件生命周期 actived 和 unActivedactived 作為緩存路由組件激活時候用,初始化的時候會默認(rèn)執(zhí)行一次 , unActived 作為路由組件緩存完成后調(diào)用。但是生命周期需要用一個 HOC 組件keepaliveLifeCycle 包裹。

          使用

          import React   from 'react'import { keepaliveLifeCycle } from 'react-keepalive-router'
          @keepaliveLifeCycleclass 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 次數(shù):{activedNumber} </div> <div> 頁面 unActived 次數(shù):{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 初始化時候進(jìn)行生命周期的綁定, 在 hoc 銷毀階段,對生命周期進(jìn)行解綁, 然后交給keeper統(tǒng)一調(diào)度,keeper通過調(diào)用實例下面的生命周期函數(shù),來實現(xiàn)緩存生命周期功能的。

          五 高階組件的注意事項

          1 謹(jǐn)慎修改原型鏈

          function HOC (Component){  const proDidMount = Component.prototype.componentDidMount   Component.prototype.componentDidMount = function(){     console.log('劫持生命周期:componentDidMount')     proDidMount.call(this)  }  return  Component}

          這樣做會產(chǎn)生一些不良后果。比如如果你再用另一個同樣會修改 componentDidMount 的 HOC 增強(qiáng)它,那么前面的 HOC 就會失效!同時,這個 HOC 也無法應(yīng)用于沒有生命周期的函數(shù)組件。

          2 繼承靜態(tài)屬性

          在用屬性代理的方式編寫HOC的時候,要注意的是就是,靜態(tài)屬性丟失的問題,前面提到了,如果不做處理,靜態(tài)方法就會全部丟失。

          手動繼承

          我們可以手動將原始組件的靜態(tài)方法copy到 hoc組件上來,但前提是必須準(zhǔn)確知道應(yīng)該拷貝哪些方法。

          function HOC(Component) {  class WrappedComponent extends React.Component {      /*...*/  }  // 必須準(zhǔn)確知道應(yīng)該拷貝哪些方法   WrappedComponent.staticMethod = Component.staticMethod  return WrappedComponent}

          引入第三方庫

          這樣每個靜態(tài)方法都綁定會很累,尤其對于開源的hoc,對原生組件的靜態(tài)方法是未知的,我們可以使用 hoist-non-react-statics 自動拷貝所有的靜態(tài)方法:

          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>}

          打印結(jié)果:

          如上就解決了,HOC跨層級捕獲ref的問題。

          4 render中不要聲明HOC

          ??錯誤寫法:

          class Index extends React.Component{  render(){     const WrapHome = HOC(Home)     return <WrapHome />  }}

          如果這么寫,會造成一個極大的問題,因為每一次HOC都會返回一個新的WrapHome,react diff會判定兩次不是同一個組件,那么每次Index 組件 render觸發(fā),WrapHome,會重新掛載,狀態(tài)會全都丟失。如果想要動態(tài)綁定HOC,請參考如下方式。

          ??正確寫法:

          const WrapHome = HOC(Home)class index extends React.Component{  render(){     return <WrapHome />  }}

          六 總結(jié)

          本文從高階組件功能為切入點(diǎn),介紹二種不同的高階組件如何編寫,應(yīng)用場景,以及實踐。涵蓋了大部分耳熟能詳?shù)拈_源高階組件的應(yīng)用場景,如果你覺得這篇文章對你有啟發(fā),最好還是按照文章中的demo,跟著敲一遍,加深印象,知道什么場景用高階組件,怎么用高階組件。

          實踐是檢驗真理的唯一標(biāo)準(zhǔn),希望大家能把高階組件起來,用起來。

          最后 , 送人玫瑰,手留余香,覺得有收獲的朋友可以給筆者點(diǎn)贊,關(guān)注一波 ,陸續(xù)更新前端超硬核文章。


          如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:

          1. 點(diǎn)個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
          2. 歡迎加我微信「TH0000666」一起交流學(xué)習(xí)...
          3. 關(guān)注公眾號「前端Sharing」,持續(xù)為你推送精選好文。

                                     


          瀏覽 85
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91久久久免费 | 东京热亚洲无码 | 夜夜撸天天 | 日韩欧美大鸡巴 | 操屄小视频 |