高階組件(Higher Order Component)

高階組件(HOC)是React開發中的特有名詞,一個函數返回一個React組件,指的就是一個React組包裹着另外一個React組件。能夠理解爲一個生產React組件的工廠。javascript

有兩種類型的HOC:html

  1. Props Proxy(pp) HOC對被包裹組件WrappedComponent的props進行操做。
  2. Inherbitance Inversion(ii)HOC繼承被包裹組件WrappedComponent

Props Proxy

一種最簡單的Props Proxy實現java

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

這裏的HOC是一個方法,接受一個WrappedComponent做爲方法的參數,返回一個PP class,renderWrappedComponent。使用的時候:react

const ListHOCInstance = ppHOC(List)
<ListHOCInstance name='instance' type='hoc' />

這個例子中,咱們將本應該傳給List的props,傳給了ppHoc返回的ListHOCInstance(PP)上,在HOC內部咱們將PP的參數直接傳給ListWrappedComponent)。這樣的就至關於在List外面加了一層代理,這個代理用於處理即將傳給WrappedComponent的props,這也是這種HOC爲何叫Props Proxy。git

在pp中,咱們能夠對WrappedComponent進行如下操做:es6

  1. 操做props(增刪改)
  2. 經過refs訪問到組件實例
  3. 提取state
  4. 用其餘元素包裹WrappedComponent

操做props

好比添加新的props給WrappedComponentgithub

const isLogin = false;

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        isNew: true,
        login: isLogin
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

WrappedComponent組件新增了兩個props:isNew和login。redux

經過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 的回調函數會在 WrappedComponent 渲染時執行,你就能夠獲得WrappedComponent的引用。這能夠用來讀取/添加實例的 props ,調用實例的方法。segmentfault

不過這裏有個問題,若是WrappedComponent是個無狀態組件,則在proc中的wrappedComponentInstance是null,由於無狀態組件沒有this,不支持ref。api

提取state

你能夠經過傳入 props 和回調函數把 state 提取出來,

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

使用的時候:

class Test extends React.Component {
    render () {
        return (
            <input name="name" {...this.props.name}/>
        );
    }
}

export default ppHOC(Test);

這樣的話,就能夠實現將input轉化成受控組件。

用其餘元素包裹 WrappedComponent

這個比較好理解,就是將WrappedComponent組件外面包一層須要的嵌套結構

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

Inheritance Inversion(反向繼承)

先看一個反向繼承(ii)的?:

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

上面例子能夠看出來Enhancer繼承了WrappedComponent,可是Enhancer能夠經過super關鍵字獲取到父類原型對象上的全部方法(父類實例上的屬性或方法則沒法獲取)。在這種方式中,它們的關係看上去被反轉(inverse)了。

咱們可使用Inheritance Inversion實現如下功能:

  1. 渲染劫持(Render Highjacking)
  2. 操做state

渲染劫持

之因此被稱爲渲染劫持是由於 HOC 控制着 WrappedComponent 的渲染輸出,能夠用它作各類各樣的事。經過渲染劫持咱們能夠實現:

  1. 在由 render輸出的任何 React 元素中讀取、添加、編輯、刪除 props
  2. 讀取和修改由 render 輸出的 React 元素樹
  3. 有條件地渲染元素樹
  4. 把樣式包裹進元素樹(就像在 Props Proxy 中的那樣)

demo1:條件渲染

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      if (!this.props.loading) {
        return super.render()
      } else {
        return <div>loading</div>    
      }
    }
  }
}

demo2:修改渲染

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

在這個例子中,若是 WrappedComponent 的輸出在最頂層有一個 input,那麼就把它的 value 設爲 「may the force be with you」

你能夠在這裏作各類各樣的事,你能夠遍歷整個元素樹,而後修改元素樹中任何元素的 props。

操做state

HOC 能夠讀取、編輯和刪除 WrappedComponent 實例的 state,若是你須要,你也能夠給它添加更多的 state。記住,這會搞亂 WrappedComponent 的 state,致使你可能會破壞某些東西。要限制 HOC 讀取或添加 state,添加 state 時應該放在單獨的命名空間裏,而不是和 WrappedComponent 的 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>
      )
    }
  }
}

問題

使用高階組件的時候,也會遇到一些問題:

靜態方法丟失

當使用高階組件包裝組件,原始組件被容器組件包裹,也就意味着新組件會丟失原始組件的全部靜態方法。

可使用hoist-non-react-statics來幫你自動處理,它會自動拷貝全部非React的靜態方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
export default (title = '默認標題') => (WrappedComponent) => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
    render() {
      return (
        <fieldset>
          <legend>{title}</legend>
          <WrappedComponent {...this.props} />
        </fieldset>
      );
    }
  }
  // 拷貝靜態方法
  hoistNonReactStatic(HOC, WrappedComponent);
  return HOC;
};

Refs屬性不能傳遞

通常來講,高階組件能夠傳遞全部的props屬性給包裹的組件,可是不能傳遞 refs 引用。由於並非像 key 同樣,refs 是一個僞屬性,React 對它進行了特殊處理。
若是你向一個由高級組件建立的組件的元素添加 ref 應用,那麼 ref 指向的是最外層容器組件實例的,而不是包裹組件。
但有的時候,咱們不可避免要使用 refs,官方給出的解決方案是:

傳遞一個ref回調函數屬性,也就是給ref應用一個不一樣的名字

const Hello = createReactClass({
  componentDidMount: function() {
    var component = this.hello;
    // ...do something with component
  },
  render() {
    return <div ref={(c) => { this.hello = c; }}>Hello, world.</div>;
  }
});

反向繼承不能保證完整的子組件樹被解析

這裏要注意的是:

  • 元素(element)是一個是用DOM節點或者組件來描述屏幕顯示的純對象,元素能夠在屬性(props.children)中包含其餘的元素,一旦建立就不會改變。咱們經過JSXReact.createClass建立的都是元素。
  • 組件(component)能夠接受屬性(props)做爲輸入,而後返回一個元素樹(element tree)做爲輸出。有多種實現方式:Class或者函數(Function)。

反向繼承不能保證完整的子組件樹被解析的意思的解析的元素樹中包含了組件(函數類型或者Class類型),就不能再操做組件的子組件了,這就是所謂的不能徹底解析。詳細介紹可參考官方博客

約定

官方文檔中也對使用高階組件有一些約定

將不相關的props屬性傳遞給包裹組件

高階組件給組件添加新特性。他們不該該大幅修改原組件的接口(譯者注:應該就是props屬性)。預期,從高階組件返回的組件應該與原包裹的組件具備相似的接口。

高階組件應該傳遞與它要實現的功能點無關的props屬性。大多數高階組件都包含一個以下的render函數:

render() {
  // 過濾掉與高階函數功能相關的props屬性,
  // 再也不傳遞
  const { extraProp, ...passThroughProps } = this.props;

  // 向包裹組件注入props屬性,通常都是高階組件的state狀態
  // 或實例方法
  const injectedProp = someStateOrInstanceMethod;

  // 向包裹組件傳遞props屬性
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

最大化使用組合

並非全部的高階組件看起來都是同樣的。有時,它們僅僅接收一個參數,即包裹組件:

const NavbarWithRouter = withRouter(Navbar);

通常而言,高階組件會接收額外的參數。在下面這個來自Relay的示例中,可配置對象用於指定組件的數據依賴關係:

const CommentWithRelay = Relay.createContainer(Comment, config);

大部分常見高階組件的函數簽名以下所示:

// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(Comment);

這是什麼?! 若是你把它剝開,你就很容易看明白究竟是怎麼回事了。

// connect是一個返回函數的函數(譯者注:就是個高階函數)
const enhance = connect(commentListSelector, commentListActions);
// 返回的函數就是一個高階組件,該高階組件返回一個與Redux store
// 關聯起來的新組件
const ConnectedComment = enhance(CommentList);

換句話說,connect 是一個返回高階組件的高階函數!

這種形式有點讓人迷惑,有點多餘,可是它有一個有用的屬性。那就是,相似 connect 函數返回的單參數的高階組件有着這樣的簽名格式, Component => Component.輸入和輸出類型相同的函數是很容易組合在一塊兒。

// 不要這樣作……
const EnhancedComponent = connect(commentSelector)(withRouter(WrappedComponent))

// ……你可使用一個功能組合工具
// compose(f, g, h) 和 (...args) => f(g(h(...args)))是同樣的
const enhance = compose(
  // 這些都是單參數的高階組件
  connect(commentSelector),
  withRouter
)
const EnhancedComponent = enhance(WrappedComponent)

connect函數產生的高階組件和其它加強型高階組件具備一樣的被用做裝飾器的能力。)

包括lodash(好比說lodash.flowRight), ReduxRamda在內的許多第三方庫都提供了相似compose功能的函數。

包裝顯示名字以便於調試

高價組件建立的容器組件在React Developer Tools中的表現和其它的普通組件是同樣的。爲了便於調試,能夠選擇一個好的名字,確保可以識別出它是由高階組件建立的新組件仍是普通的組件。

最經常使用的技術就是將包裹組件的名字包裝在顯示名字中。因此,若是你的高階組件名字是 withSubscription,且包裹組件的顯示名字是 CommentList,那麼就是用 withSubscription(CommentList)這樣的顯示名字:

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

參考文檔

  1. 深刻理解 React 高階組件
  2. Higher-Order Components
  3. React 高階組件淺析
  4. React 高階組件(HOC)入門指南
  5. React進階——使用高階組件(Higher-order Components)優化你的代碼
相關文章
相關標籤/搜索