React 高階組件(HOC)入門指南

  以前的文章React Mixins入門指南介紹了React Mixin的使用。在實際使用中React Mixin的做用仍是很是強大的,可以使得咱們在多個組件中共用相同的方法。可是工程中大量使用Mixin也會帶來很是多的問題。Dan Abramov在文章Mixins Considered Harmful 介紹了Mixin帶來的一些問題,總結下來主要是如下幾點:javascript

  • 破壞組件封裝性: Mixin可能會引入不可見的屬性。例如在渲染組件中使用Mixin方法,給組件帶來了不可見的屬性(props)和狀態(state)。而且Mixin可能會相互依賴,相互耦合,不利於代碼維護。
  • 不一樣的Mixin中的方法可能會相互衝突

  爲了處理上述的問題,React官方推薦使用高階組件(High Order Component)html

高階組件(HOC)

  剛開始學習高階組件時,這個概念就透漏着高級的氣味,看上去就像是一種先進的編程技術的一個深奧術語,畢竟名字裏就有"高階"這種字眼,實質上並非如此。高階組件的概念應該是來源於JavaScript的高階函數:java

高階函數就是接受函數做爲輸入或者輸出的函數react

  這麼看來柯里化也是高階函數了。React官方定義高階組件的概念是:git

A higher-order component is a function that takes a component and returns a new component.github

  (本人也翻譯了React官方文檔的Advanced Guides部分,官方的高階組件中文文檔戳這裏)編程

  這麼看來,高階組件僅僅只是是一個接受組件組做輸入並返回組件的函數。看上去並無什麼,那麼高階組件能爲咱們帶來什麼呢?首先看一下高階組件是如何實現的,一般狀況下,實現高階組件的方式有如下兩種:segmentfault

  1. 屬性代理(Props Proxy)
  2. 反向繼承(Inheritance Inversion)

屬性代理

  又是一個聽起來很高大上的名詞,實質上是經過包裹原來的組件來操做props,舉個簡單的例子:app

import React, { Component } from 'React';
//高階組件定義
const HOC = (WrappedComponent) =>
  class WrapperComponent extends Component {
    render() {
      return <WrappedComponent {...this.props} />; } } //普通的組件 class WrappedComponent extends Component{ render(){ //.... } } //高階組件使用 export default HOC(WrappedComponent)複製代碼

  上面的例子很是簡單,但足以說明問題。咱們能夠看見函數HOC返回了新的組件(WrapperComponent),這個組件原封不動的返回做爲參數的組件(也就是被包裹的組件:WrappedComponent),並將傳給它的參數(props)所有傳遞給被包裹的組件(WrappedComponent)。這麼看起來好像並無什麼做用,其實屬性代理的做用仍是很是強大的。ide

操做props

  咱們看到以前要傳遞給被包裹組件WrappedComponent的屬性首先傳遞給了高階組件返回的組件(WrapperComponent),這樣咱們就得到了props的控制權(這也就是爲何這種方法叫作屬性代理)。咱們能夠按照須要對傳入的props進行增長、刪除、修改(固然修改帶來的風險須要你本身來控制),舉個例子:

const HOC = (WrappedComponent) =>
    class WrapperComponent extends Component {
        render() {
            const newProps = {
                name: 'HOC'
            }
            return <WrappedComponent {...this.props} {...newProps} />; } }複製代碼

  在上面的例子中,咱們爲被包裹組件(WrappedComponent)新增長了固定的name屬性,所以WrappedComponent組件中就會多一個name的屬性。

得到refs的引用

  咱們在屬性代理中,能夠輕鬆的拿到被包裹的組件的實例引用(ref),例如:

import React, { Component } from 'React';
 
const HOC = (WrappedComponent) =>
    class wrapperComponent extends Component {
        storeRef(ref) {
            this.ref = ref;
        }
        render() {
            return <WrappedComponent {...this.props} ref = {::this.storeRef} />; } }複製代碼

  上面的例子中,wrapperComponent渲染接受後,咱們就能夠拿到WrappedComponent組件的實例,進而實現調用實例方法的操做(固然這樣會在必定程度上是反模式的,不是很是的推薦)。

抽象state

  屬性代理的狀況下,咱們能夠將被包裹組件(WrappedComponent)中的狀態提到包裹組件中,一個常見的例子就是實現不受控組件受控的組件的轉變(關於不受控組件和受控組件戳這裏)

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

const HOC = (WrappedComponent) =>
    class extends 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} />;
        }
    }複製代碼

  上面的例子中經過高階組件,咱們將不受控組件(WrappedComponent)成功的轉變爲受控組件.

用其餘元素包裹組件

  咱們能夠經過相似:

render(){
        <div>
            <WrappedComponent {...this.props} /> </div>
    }複製代碼

  這種方式將被包裹組件包裹起來,來實現佈局或者是樣式的目的。

  在屬性代理這種方式實現的高階組件,以上述爲例,組件的渲染順序是: 先WrappedComponent再WrapperComponent(執行ComponentDidMount的時間)。而卸載的順序是先WrapperComponent再WrappedComponent(執行ComponentWillUnmount的時間)。

反向繼承

  反向繼承是指返回的組件去繼承以前的組件(這裏都用WrappedComponent代指)

const HOC = (WrappedComponent) =>
  class extends WrappedComponent {
    render() {
      return super.render();
    }
  }複製代碼

   咱們能夠看見返回的組件確實都繼承自WrappedComponent,那麼全部的調用將是反向調用的(例如:super.render()),這也就是爲何叫作反向繼承。

渲染劫持

  渲染劫持是指咱們能夠有意識地控制WrappedComponent的渲染過程,從而控制渲染控制的結果。例如咱們能夠根據部分參數去決定是否渲染組件:

const HOC = (WrappedComponent) =>
  class extends WrappedComponent {
    render() {
      if (this.props.isRender) {
        return super.render();
      } else {
        return null;
      }
    }
  }複製代碼

  甚至咱們能夠修改修改render的結果:

//例子來源於《深刻React技術棧》

const HOC = (WrappedComponent) =>
    class 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;
    }
}
class WrappedComponent extends Component{
    render(){
        return(
            <input value={'Hello World'} /> ) } } export default HOC(WrappedComponent) //實際顯示的效果是input的值爲"may the force be with you"複製代碼

  上面的例子中咱們將WrappedComponent中的input元素value值修改成:may the force be with you。咱們能夠看到先後elementTree的區別:
elementsTree:

elementsTree

newElementsTree:

newElementsTree

  在反向繼承中,咱們能夠作很是多的操做,修改state、props甚至是翻轉Element Tree。反向繼承有一個重要的點: 反向繼承不能保證完整的子組件樹被解析,開始我對這個概念也不理解,後來在看了React Components, Elements, and Instances這篇文章以後對這個概念有了本身的一點體會。
React Components, Elements, and Instances這篇文章主要明確了一下幾個點:

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

  因此, 反向繼承不能保證完整的子組件樹被解析的意思的解析的元素樹中包含了組件(函數類型或者Class類型),就不能再操做組件的子組件了,這就是所謂的不能徹底解析。舉個例子:

import React, { Component } from 'react';

const MyFuncComponent = (props)=>{
    return (
        <div>Hello World</div>
    );
}

class MyClassComponent extends Component{

    render(){
        return (
            <div>Hello World</div>
        )
    }

}

class WrappedComponent extends Component{
    render(){
        return(
            <div> <div> <span>Hello World</span> </div> <MyFuncComponent /> <MyClassComponent /> </div>

        )
    }
}

const HOC = (WrappedComponent) =>
    class extends WrappedComponent {
        render() {
            const elementsTree = super.render();
            return elementsTree;
        }
    }

export default HOC(WrappedComponent);複製代碼

element tree1

element tree2

  咱們能夠查看解析的元素樹(element tree),div下的span是能夠被徹底被解析的,可是MyFuncComponentMyClassComponent都是組件類型的,其子組件就不能被徹底解析了。

操做props和state

  在上面的圖中咱們能夠看到,解析的元素樹(element tree)中含有propsstate(例子的組件中沒有state),以及refkey等值。所以,若是須要的話,咱們不只能夠讀取propsstate,甚至能夠修改增長、修改和刪除。

  在某些狀況下,咱們可能須要爲高階屬性傳入一些參數,那咱們就能夠經過柯里化的形式傳入參數,例如:

import React, { Component } from 'React';

const HOCFactoryFactory = (...params) => {
    // 能夠作一些改變 params 的事
    return (WrappedComponent) => {
        return class HOC extends Component {
            render() {
                return <WrappedComponent {...this.props} />; } } } }複製代碼

能夠經過下面方式使用:

HOCFactoryFactory(params)(WrappedComponent)複製代碼

  這種方式是否是很是相似於React-Redux庫中的connect函數,由於connect也是相似的一種高階函數。反向繼承不一樣於屬性代理的調用順序,組件的渲染順序是: 先WrappedComponent再WrapperComponent(執行ComponentDidMount的時間)。而卸載的順序也是先WrappedComponent再WrapperComponent(執行ComponentWillUnmount的時間)。

HOC和Mixin的比較

  借用《深刻React技術棧》一書中的圖:

HOCandMixin

  高階組件屬於函數式編程(functional programming)思想,對於被包裹的組件時不會感知到高階組件的存在,而高階組件返回的組件會在原來的組件之上具備功能加強的效果。而Mixin這種混入的模式,會給組件不斷增長新的方法和屬性,組件自己不只能夠感知,甚至須要作相關的處理(例如命名衝突、狀態維護),一旦混入的模塊變多時,整個組件就變的難以維護,也就是爲何如此多的React庫都採用高階組件的方式進行開發。

相關文章
相關標籤/搜索