React
高階組件(HOC
),對於不少react
開發者來講並不陌生,它是靈活使用react
組件的一種技巧,高階組件自己不是組件,它是一個參數爲組件,返回值也是一個組件的函數。高階做用用於強化組件,複用邏輯,提高渲染性能等做用。高階組件也並非很難理解,其實接觸事後仍是蠻簡單的,接下來我將按照,高階組件理解?,高階組件具體怎麼使用?應用場景, 高階組件實踐(源碼級別) 爲突破口,帶你們詳細瞭解一下高階組件。本文篇幅比較長,建議收藏觀看前端
咱們帶着問題去開始今天的討論:vue
hoc
怎麼處理靜態屬性,跨層級ref
等問題?高階組件(HOC)是 React 中用於複用組件邏輯的一種高級技巧。HOC 自身不是 React API 的一部分,它是一種基於 React 的組合特性而造成的設計模式。node
原型圖react
react-mixins
在react
初期提供一種組合方法。經過React.createClass
,加入mixins
屬性,具體用法和vue
中mixins
類似。具體實現以下。es6
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
會帶來一些負面的影響。redux
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)
複製代碼
原型圖api
在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
。
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
的誕生,一大部分緣由是解決無狀態組件沒有state
和邏輯難以複用問題。hooks
能夠將一段邏輯封裝起來,作到開箱即用,我這裏就很少講了,接下來會出react-hooks
原理的文章,完成react-hooks
三部曲。感興趣的同窗能夠看筆者的另外二篇文章,裏面詳細介紹了react-hooks
複用代碼邏輯的原則和方案。
傳送門:
玩轉react-hooks,自定義hooks設計模式及其實戰
組件是把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
組件懶加載。
我會針對高階組件的初衷展開,詳細介紹其原理已經用法。跟上個人思路,咱們先來看一下,高階組件如何在咱們的業務組件中使用的。
HOC
使用指南是很是簡單的,只須要將咱們的組件進行包裹就能夠了。
對於class
聲明的有狀態組件,咱們能夠用裝飾器模式,對類組件進行包裝:
@withStyles(styles)
@withRouter
@keepaliveLifeCycle
class Index extends React.Componen{
/* ... */
}
複製代碼
咱們要注意一下包裝順序,越靠近Index
組件的,就是越內層的HOC
,離組件Index
也就越近。
對於無狀態組件(函數聲明)咱們能夠這麼寫:
function Index(){
/* .... */
}
export default withStyles(styles)(withRouter( keepaliveLifeCycle(Index) ))
複製代碼
對於不須要傳遞參數的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
也就易如反掌。
經常使用的高階組件有兩種方式正向的屬性代理和反向的組件繼承,二者以前有一些共性和區別。接下具體介紹二者區別,在第三部分會詳細介紹具體實現。
所謂正向屬性代理,就是用組件包裹一層代理組件,在代理組件上,咱們能夠作一些,對源組件的代理操做。在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
。
這個是高階組件最經常使用的功能,承接上層的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 } />
}
}
複製代碼
效果
高階組件能夠將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)
複製代碼
效果
控制渲染是高階組件的一個很重要的特性,上邊說到的兩種高階組件,都能完成對組件渲染的控制。具體實現仍是有區別的,咱們一塊兒來探索一下。
對於屬性代理的高階組件,雖然不能在內部操控渲染狀態,可是能夠在外層控制當前組件是否渲染,這種狀況應用於,權限隔離,懶加載 ,延時加載等場景。
實現一個動態掛載組件的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
,能夠說是偷天換日,不亦樂乎。
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
,感興趣的同窗,能夠抽時間研究一下。
高階組件除了上述兩種功能以外,還能夠賦能組件,好比加一些額外生命週期
,劫持事件,監控日誌等等。
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>
}
}
複製代碼
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>
}
複製代碼
效果
對於屬性代理咱們雖然不能直接獲取組件內的狀態,可是咱們能夠經過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
對原有組件的賦能。
上面我分別按照hoc
主要功能,強化props , 控制渲染 ,賦能組件 三個方向對HOC
編寫作了一個詳細介紹,和應用場景的介紹,目的讓你們在理解高階組件的時候,更明白何時會用到?,怎麼樣去寫?` 裏面涵蓋的知識點我總一個總結。
對於屬性代理HOC,咱們能夠:
對於反向代理的HOC,咱們能夠:
每一個應用場景,我都舉了例子🌰🌰,你們能夠結合例子深刻了解一下其原理和用途。
hoc
的應用場景有不少,也有不少好的開源項目,供咱們學習和參考,接下來我真對三個方向上的功能用途,分別從源碼角度解析HOC
的用途。
用過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
咱們稍後會講到。
因爲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
專門負責訂閱消息。
以前筆者寫了一個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
經過調用實例下面的生命週期函數,來實現緩存生命週期功能的。
function HOC (Component){
const proDidMount = Component.prototype.componentDidMount
Component.prototype.componentDidMount = function(){
console.log('劫持生命週期:componentDidMount')
proDidMount.call(this)
}
return Component
}
複製代碼
這樣作會產生一些不良後果。好比若是你再用另外一個一樣會修改 componentDidMount
的 HOC
加強它,那麼前面的 HOC
就會失效!同時,這個 HOC
也沒法應用於沒有生命週期的函數組件。
在用屬性代理的方式編寫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
}
複製代碼
高階組件的約定是將全部 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
的問題。
🙅錯誤寫法:
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
,跟着敲一遍,加深印象,知道什麼場景用高階組件,怎麼用高階組件。
實踐是檢驗真理的惟一標準
,但願你們能把高階組件碼
起來,用起來。
最後 , 送人玫瑰,手留餘香,以爲有收穫的朋友能夠給筆者點贊,關注一波 ,陸續更新前端超硬核文章。
react進階系列
840+
贊👍react源碼系列
react-hooks系列
玩轉react-hooks,自定義hooks設計模式及其實戰 160+
👍贊
react-hooks如何使用 90+
贊👍
開源項目系列
250+
贊 👍