[譯] 深刻 React 高階組件

原文: https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3ejavascript

概要

本文面向想要探索 HOC 模式的進階用戶,若是你是 React 的初學者則應該從官方文檔開始。高階組件(Higher Order Components)是一種很棒的模式,已被不少 React 庫證明是很是有價值的。在本文中,咱們首先回顧一下 HOC 是什麼、有什麼用、有何侷限,以及是如何實現它的。css

在附錄中,檢視了相關的話題,這些話題並不是 HOC 的核心,但我認爲應該說起。html

本文旨在儘可能詳細的論述,以便於讀者查閱;並假定你已經知曉 ES6。java

走你!react

高階組件是什麼?

高階組件就是包裹了其餘 React Component 的組件git

一般,這個模式被實現爲一個函數,基本算是個類工廠方法(yes, a class factory!),其函數簽名用 haskell 風格的僞代碼寫出來就是這樣的:github

hocFactory:: W: React.Component => E: React.Component
複製代碼

W (WrappedComponent) 是被包裹的 React.Component;而函數返回的 E (Enhanced Component) 則是新獲得的 HOC,也是個 React.Component。bash

定義中的「包裹」是一種有意的模糊,意味着兩件事情:app

  • 屬性代理:由 HOC 操縱那些被傳遞給被包裹組件 W 的 props
  • 繼承反轉:HOC 繼承被包裹組件 W

後面會詳述這兩種模式的。函數

HOC 能作什麼?

在大的維度上 HOC 能用於:

  • 代碼重用和邏輯抽象
  • render 劫持
  • state 抽象和操縱
  • 操縱屬性(props)

後面將會看到這些類目的細節,但首先來學習一下實現 HOC 的方式,由於實現方式決定了 HOC 實際能作的事情。

HOC 工廠實現

屬性代理(PP)和繼承反轉(II)。二者皆提供了不一樣的途徑以操縱被包裹的組件。

屬性代理

屬性代理(Props Proxy)能夠用如下方式簡單的實現:

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      return <WrappedComponent {...this.props}/> } } } 複製代碼

此處關鍵的部分在於 HOC 的 render() 方法返回了一個被包裹組件的 React Element。同時,將 HOC 接受到的屬性傳遞給了被包裹的組件,所以稱爲**「屬性代理」**。

注意:

<WrappedComponent {...this.props}/>
// 等價於
React.createElement(WrappedComponent, this.props, null)
複製代碼

二者都會建立一個 React Element,用於描述 React 在其一致性比較過程當中應該渲染什麼。

瞭解更多:

關於 React Elment vs Components 的內容能夠查看
https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html

一致性比較過程
http://www.css88.com/react/docs/reconciliation.html

能夠用屬性代理作些什麼?

  • 操縱屬性
  • 經過 refs 訪問實例
  • 抽象 state
  • 包裹組件

操縱屬性

能夠對傳遞給被包裹組件的屬性進行增刪查改。但刪除或編輯重要屬性時要謹慎,應合理設置 HOC 的命名空間以避免影響被包裹組件。

例子:增長新屬性。應用中經過 this.props.user 將能夠獲得已登陸用戶

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        user: currentLoggedInUser
      }
      return <WrappedComponent {...this.props} {...newProps}/> } } } 複製代碼

經過 refs 訪問實例

能夠經過 ref 訪問到 this(被包裹組件的實例),但這須要 ref 所引用的被包裹組件運行一次完整的初始化 render 過程,這就意味着要從 HOC 的 render 方法中返回被包裹組件的元素,並讓 React 完成其一致性比較過程,而 ref 能引用該組件的實例就行了。

例子:下例中展現瞭如何經過 refs 訪問到被包裹組件的實例方法和實例自己

function refsHOC(WrappedComponent) {
  return class RefsHOC extends React.Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method()
    }
    
    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
      return <WrappedComponent {...props}/> } } } 複製代碼

當被包裹組件被渲染,ref 回調就將執行,由此就能得到其實例的引用。這能夠用於讀取、增長實例屬性,或調用實例方法。

抽象 state

經過提供給被包裹組件的屬性和回調,能夠抽象 state,這很是相似於 smart 組件是如何處理 dumb 組件的。

關於上述兩種組件能夠參閱:
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

例子:在下面這個抽象 state 的例子裏咱們簡單的將 value 和 onChange 處理函數從 name 輸入框中抽象出來。之因此說「簡單」是由於這很是廣泛,但你必須明白這一點。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }
      
      this.onNameChange = this.onNameChange.bind(this)
    }
    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }
    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange
        }
      }
      return <WrappedComponent {...this.props} {...newProps}/> } } } 複製代碼

用起來可能會是這樣的:

@ppHOC
class Example extends React.Component {
  render() {
    return <input name="name" {...this.props.name}/>
  }
}
複製代碼

因而這個輸入框就自動成爲了一個受控組件。

關於受控組件:
https://mp.weixin.qq.com/s/I3aPxyZA_iArUDmsXtXGcw

包裹組件

能夠利用組件的包裹,實現樣式定義、佈局或其餘目標。一些基礎用法能夠由普通的父組件完成(參閱附錄B),但如前所述,用 HOC 能夠更加靈活。

例子:爲定義樣式而實現的包裹

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      return (
        <div style={{display: 'block'}}> <WrappedComponent {...this.props}/> </div> ) } } } 複製代碼

繼承反轉

繼承反轉 (Inheritance Inversion) 只須要這樣實現就能夠:

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}
複製代碼

如你所見,被返回的 HOC 類(強化過的類)繼承了被包裹的組件。之因此被稱爲「繼承反轉」是由於,被包裹組件並不去繼承強化類,而是被動的讓強化類繼承。經過這種方式,兩個類的關係看起來反轉了。

繼承反轉使得 HOC 能夠用 this 訪問被包裹組件的實例,這意味着能夠訪問 state、props、組件生命週期鉤子,以及 render 方法

這裏並不深刻探討能夠在生命週期鉤子中實現的細節,由於那屬於 React 的範疇。但要知道經過繼承反轉能夠爲被包裹組件建立新的生命週期鉤子;並記住老是應該調用 super.[lifecycleHook] 以確保不會破壞被包裹的組件。

一致性比較過程

在深刻以前咱們大概說一下這些理論。

一致性比較
https://facebook.github.io/react/docs/reconciliation.html

React Elements 描述了 React 運行其一致性比較過程時,什麼會被渲染。

React Elements 能夠是兩種類型:字符串和函數。字符串類型的 React Elements(STRE)表明 DOM 節點,函數類型的 React Elements(FTRE)表明繼承自 React.Component 的組件。

React 元素和組件
https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html

在 React 的一致性比較過程(最終結果是 DOM 元素)中,FTRE 會被處理成一棵完整的 STRE 樹。

之因此很重要,就在於這意味着繼承反轉高階組件並不保證處理完整的子樹

後面學習到 render 劫持的時候將會證實其重要性。

能夠用繼承反轉作些什麼?

  • render 劫持
  • 操縱 state

render 劫持

稱之爲「render 劫持」是由於 HOC 控制了被包裹組件的 render 輸出,並能對其作任何事情。

在 render 劫持中能夠:

  • 在任何 render 輸出的 React Elements 上增刪查改 props
  • 讀取並修改 render 輸出的 React Elements 樹
  • 條件性顯示元素樹
  • 出於定製樣式的目的包裹元素樹(正如屬性代理中展現的)

*用 render 引用被包裹組件的 render 方法

不能對被包裹組件的實例編輯或建立屬性,由於一個 React Component 沒法編輯其收到的 props,但能夠改變被 render 方法輸出的元素的屬性。

就如咱們以前學到的,繼承反轉 HOC 不保證處理完整的子樹,這意味着 render 劫持技術有一些限制。經驗法則是,藉助於 render 劫持,能夠很少很多的操做被包裹組件的 render 方法輸出的元素樹。若是那個元素數包含了一個函數類型的 React Component,那就沒法操做其子組件(被 React 的一致性比較過程延遲到真正渲染到屏幕上時)。

例子1:條件性渲染

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render()
      } else {
        return null
      }
    }
  }
}
複製代碼

例子2:修改 render 輸出的元素樹

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      const elementsTree = super.render()
      let newProps = {};
      if (elementsTree && elementsTree.type === 'input') {
        newProps = {value: 'may the force be with you'}
      }
      const props = Object.assign({}, elementsTree.props, newProps)
      const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children)
      return newElementsTree
    }
  }
}
複製代碼

本例中,若是由 render 輸出的被包裹組件有一個 input 頂級元素,就改變其 value。

能夠在這裏作任何事情,能夠遍歷整個元素樹並改變其中的任何一個元素屬性。

注意:不能經過屬性代理劫持 render

雖然經過 WrappedComponent.prototype.render 訪問 render 方法是可能的,但這樣一來你就要模擬被包裹組件的實例及其屬性,並本身處理組件生命週期而非依靠 React 去解決。以個人經驗來講這是得不償失的,若是要劫持 render 應該用繼承反轉而非屬性代理。要記住 React 內在地處置組件實例,而你只能經過 this 或 refs 來處理實例。

操縱 state

HOC 能夠讀取、編輯和刪除被包裹組件實例的 state,也能夠按需增長更多的 state。要謹記若是把 state 搞亂會很糟糕。大部分 HOC 應該限制讀取或增長 state,然後者(譯註:增長 state)應該使用命名空間以避免和被包裹組件的 state 搞混。

例子:對訪問被包裹組件的 props 和 state 的調試

export function IIHOCDEBUGGER(WrappedComponent) {
  return class II extends WrappedComponent {
    render() {
      return (
        <div> <h2>HOC Debugger Component</h2> <p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre> <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre> {super.render()} </div>
      )
    }
  }
}
複製代碼

該 HOC 將被包裹組件嵌入其餘元素中,並顯示了其 props 和 state。

命名

使用 HOC 時,就失去了被包裹組件原有的名字,可能會影響開發和調試。

人們一般的作法就是用原有名字加上些什麼來命名 HOC。下面的例子取自 React-Redux

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//or
class HOC extends ... {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`
  ...
}
複製代碼

而 getDisplayName 函數的定義以下:

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || 
         WrappedComponent.name || 
         ‘Component’
}
複製代碼

其實你都不須要本身寫一遍這個函數,recompose 庫(https://github.com/acdlite/recompose)已經提供了。

附錄 A:HOC 和參數

如下爲能夠跳過的選讀內容

在 HOC 中能夠善用參數。這原本已經在上面全部例子中隱含的出現過,而且對於中級 JS 開發者也已經稀鬆日常了,可是本着知無不言的原則,仍是快速過一遍吧。

例子:結合屬性代理和 HOC 參數,須要關注的是 HOCFactoryFactory 函數

function HOCFactoryFactory(...params){
  // do something with params
  return function HOCFactory(WrappedComponent) {
    return class HOC extends React.Component {
      render() {
        return <WrappedComponent {...this.props}/> } } } } 複製代碼

能夠這樣使用:

HOCFactoryFactory(params)(WrappedComponent)
//or
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}
複製代碼

附錄 B:和父組件的區別

如下爲能夠跳過的選讀內容

有一些子組件的 React 組件稱爲父組件,React 有一些訪問和控制組件子成員的 API。

例子:父組件訪問子組件

class Parent extends React.Component {
    render() {
      return (
        <div>
          {this.props.children}
        </div>
      )
    }
  }
}

render((
  <Parent>
    {children}
  </Parent>
), mountNode)
複製代碼

相比於 HOC,來細數一下父組件能作和不能作的:

  • render 劫持(在繼承反轉中看見過)
  • 控制內部 props(一樣在繼承反轉中看見過)
  • 抽象 state,但存在缺點。將沒法在外部訪問父元素的 state,除非特地爲止建立鉤子。這限制了其實用性
  • 包裹新的 React Elements。這多是父組件惟一強於 HOC 的用例,雖然 HOC 也能作到
  • 操縱子組件有一些陷阱。好比說若是 children 的根一級並不僅有單一的子組件(多於一個的第一級子組件),你就得添加一個額外的元素來收納全部子元素,這會讓你的代碼有些冗繁。在 HOC 中一個單一的頂級子組件是被 React/JSX 的約束所保證的。

一般,父組件的作法沒有 HOC 那麼 hacky,但上述列表是其相比於 HOC 的不靈活之處。

結語

但願閱讀本文後你能對 React HOC 多一些瞭解。在不一樣的庫中,HOC 都被證實是頗有價值並不是常好用的。

React 帶來了不少創新,人們普遍應用着 Radium、React-Redux、React-Router 等等,也很好的印證了這一點。

----------------------------------------

長按二維碼或搜索 fewelife 關注咱們哦

相關文章
相關標籤/搜索