React高階指南之高階元件

劉源泉發表於2018-08-18

路漫漫其修遠兮,吾將上下而求索。— 屈原《離騷》

寫在前面

高階元件不是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)
複製程式碼

結果如下:

React高階指南之高階元件

HOC元件就是高階元件,它包裹了傳入的Demo元件,並給他新增了一個標題

假設有三個Demo元件,Demo1,Demo2,Demo3它們之間的區別就是元件的內容不一樣(這樣做是方便做演示),它們都用HOC進行包裹了,結果發現包裹之後的元件名稱都為HOC,這時候我們需要區分包裹之後的三個高階元件,

React高階指南之高階元件

給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
}
複製程式碼

React高階指南之高階元件

再看看三個高階元件的名稱都不一樣了,但是我們想讓標題不寫死,而是可以動態傳入可以嗎?當然是可以的,我們可以藉助函式的柯里化實現,我們對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)
複製程式碼

結果如下:

React高階指南之高階元件

我們可以藉助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>
    )
  }
}
複製程式碼

顯示如下:

React高階指南之高階元件

對上面的高階元件和被包裹元件進行了改進,高階元件內部可以生成一個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>
    )
  }
}
複製程式碼

最後顯示如下:

React高階指南之高階元件

注意高階元件內部沒有了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()
複製程式碼

React高階指南之高階元件

我們有兩種辦法可以解決:

  • 手動複製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
}
複製程式碼

這樣就不會報錯了

React高階指南之高階元件

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)
  }
}
複製程式碼

結果列印的資訊如下:

React高階指南之高階元件

我們的本意是把ref傳遞給內層包裹的WrappedComponent,結果列印的確是外層的HOC,我們再去看看React元件樹的資訊

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高階指南之高階元件

完美解決!

最後

本文只是簡單的介紹了下高階元件的書寫和其兩種實現方式,及一些要避免的坑,如果想對高階元件有個更系統的瞭解推薦去閱讀 React官方文件的高階元件,還有可以閱讀一些原始碼:如react-redux的connect,antd的Form.create

React學習之路很有很長

你們的打賞是我寫作的動力

微信
支付寶

相關文章