React 高階組件淺析

 

最近在一些項目中遇到高階組件的身影,不是很瞭解,於是深入鑽研了一番,以下權當是學習記錄了~

 

Mixin

在談及高階組件之前,我們先來講講它的前身 mixin ~
mixin 的作用是:如果多個組件中包含相同的方法(包括普通函數和組件生命週期函數),就可以把這一類函數提取到 mixin 中,然後在需要公共方法的組件中使用 mixin, 就可以避免每個組件都去聲明一次,從而達到複用。

 

React 在早期是使用 createClass 來創建一個 Component 的,而且 createClass 支持 mixin 屬性,最常見的就是 react-addons-pure-render-mixin 庫提供的 PureRenderMixin 方法,用來減少組件使用中一些不必要的渲染,使用方式如下:

 

import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return <div>{this.props.name}</div>;
  }
});

 

和需要在每一個組件中都重複實現一遍 PureRenderMixin 中淺比較的邏輯相比,上面 mixin 中的使用顯得更加簡便和明瞭,同時減少了代碼的冗餘和重複。

 

minin 既可以定義多個組件中共享的工具方法,同時還可以定義一些組件的生命週期函數(例如上例的 shouldComponentUpdate), 以及初始的 props 和 states。

 

如下所示:

 

var propsMixin1 = {
  getDefaultProps: () => {
    return {
      name: "Amy"
    };
  }
};

var propsMixin2 = {
  getDefaultProps: () => {
    return {
      title: "mixin"
    };
  }
};

var MixinExample = createReactClass({
  mixins: [propsMixin1, propsMixin2],
  render: function() {
    return (
      <div>
        <p>{this.props.name}</p>
        <p>{this.props.title}</p>
      </div>
    );
  }
});

 

但是在使用 mixin 的時候,會有如下的幾點需要注意:

 

  • 不同 mixin 中有相同的函數

    • 組件中使用多個 mixin, 同時不同 mixin 中定義了相同的工具函數,此時會報錯(而不是前者覆蓋後者)

    • 組件中使用多個 mixin, 同時 mixin 中定義了相同的組件生命週期函數,不會報錯,此時會按傳給 createClass 的 mixin 數組順序依次調用,全部調用結束後再調用組件內部的相同的生命週期

  • 不同 mixin 中設置 props 或者 states

    • 組件中含有多個 mixin,不同的 mixin 中默認 props 或初始 state 中存在相同的 key 值時,React 會拋出異常

    • 組件中含有多個 mixin, 不同的 mixin 中默認 props 或初始 state 中存在不同的 key 值時,則默認 props 和初始 state 都會被合併。

 

附上具體示例代碼地址

 

雖然 mixin 在一定程度上解決了 React 實踐中的一些痛點,但是 React 從 v0.13.0 開始,ES6 class 組件寫法中不支持 mixins, 但是還是可以使用 createClass 來使用 mixin。之後,React 社區提出了一種新的方式來取代 mixin,那就是高階組件 Higher-Order Components。

 

高階組件

高階組件 (Higher-Order Components) 是接受一個組件作爲參數,然後經過一些處理,返回一個相對增強的組件的函數。它是 React 中的一種模式,而不是 API 的一部分。React 官方給出一個公式描述如下:

 

const EnhancedComponent = higherOrderComponent(WrappedComponent);

 

一個最簡單的 HOC 例子如下:

 

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

class Example extends React.PureComponent {
  render() {
    return (
      <div>
        <p>{this.props.age}</p>
      </div>
    );
  }
}

const HocComponent = HOC(Example);
ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

 

高階組件的適用場景

 

它的使用場景有如下幾點:

  • 需要抽離可複用的代碼邏輯

  • 渲染劫持

  • 更改 state

  • 組裝修改 props

 

高階組件的實現方式

高階組件有兩種實現方式: 屬性代理 (Props Proxy) 和反向繼承 (Inheritance Inversion)

 

屬性代理

屬性代理是指所有的數據都是從最外層的 HOC 中傳給被包裹的組件,它有權限對傳入的數據進行修改,對於被包裹組件來說,HOC 對傳給自己的屬性 (Props) 起到了一層代理作用。

 

屬性代理可以實現如下一些功能:

 

  • 更改 props

 

class Example extends React.PureComponent {
  constructor(props) {
    super(props);
  }

  render() {
    const { name, age, github } = this.props;
    return (
      <div>
        <p>{name}</p>
        <p>{age}</p>
        <p>{github}</p>
      </div>
    );
  }
}

function HOC(WrappedComponent) {
  class EnhancedComponent extends React.PureComponent {
    render() {
      const props = Object.assign({}, this.props, {
        name: "SunShinewyf",
        github: "http://github.com/SunShinewyf"
      });
      return <WrappedComponent {...props} />;
    }
  }
  return EnhancedComponent;
}

const HocComponent = HOC(Example);

ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

 

如上面的例子中,HOC 對最外層傳入的 props 進行了二次組裝,擴展了 props 的數據能力。

 

通過 refs 獲取被包裹的組件實例

 

class Example extends React.PureComponent {
  constructor(props) {
    super(props);
    this.consoleFun.bind(this);
  }

  consoleFun() {
    console.log("hello world");
  }

  render() {
    const { age } = this.props;
    return (
      <div>
        <p>{age}</p>
      </div>
    );
  }
}

function HOC(WrappedComponent) {
  class EnhancedComponent extends React.PureComponent {
    initFunc(instance) {
      instance.consoleFun();
    }
    render() {
      const props = Object.assign({}, this.props, {
        ref: this.initFunc.bind(this)
      });
      return <WrappedComponent {...props} />;
    }
  }
  return EnhancedComponent;
}

const HocComponent = HOC(Example);

ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

 

如果想要在 HOC 中執行被包裹組件的一些方法,就可以在 props 上組裝一下 ref 這個屬性,就可以獲取到被包裹組件的實例,從而獲取到實例的 props 以及它的方法。

 

組裝被包裹組件(WrappedComponent)

 

function HOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      <div>  //添加一些樣式
      return <WrappedComponent {...this.props}/>
      </div>
    }
  }
}

 

這個比較簡單,不詳述~

 

反向繼承 (Inheritance Inversion)

反向繼承是指 HOC 繼承被包裹組件,這樣被包裹的組件 (WrappedComponent) 就是 HOC 的父組件了,子組件就可以直接操作父組件的所有公開的方法和字段。

 

反向繼承可以實現如下功能:

 

對 WrappedComponent 的所有生命週期函數進行重寫,或者修改其 props 或者 state

 

class Example extends React.PureComponent {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    console.log("wrappedComponent did mount");
  }

  render() {
    const { age } = this.props;
    return (
      <div>
        <p>{age}</p>
      </div>
    );
  }
}

function HOC(WrapperComponent) {
  return class Inheritance extends WrapperComponent {
    componentDidMount() {
      console.log("HOC did mount");
      super.componentDidMount();
    }
    render() {
      return super.render();
    }
  };
}

const HocComponent = HOC(Example);

ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

// HOC did mount
// wrappedComponent did mount

 

由上面可以看到,HOC 中定義的生命週期方法可以訪問到 WrappedComponent 中的生命週期方法。兩者的執行順序由代碼的執行順序決定。

 

劫持渲染

 

class Example extends React.PureComponent {
  constructor(props) {
    super(props);
  }

  render() {
    const { age } = this.props;
    return <input />;
  }
}

function HOC(WrapperComponent) {
  return class Inheritance extends WrapperComponent {
    render() {
      const elementsTree = super.render();
      let newProps = {};
      if (elementsTree && elementsTree.type === "input") {
        newProps = { defaultValue: "the initialValue of input" };
      }
      const props = Object.assign({}, elementsTree.props, newProps);
      const newElementsTree = React.cloneElement(
        elementsTree,
        props,
        elementsTree.props.children
      );
      return newElementsTree;
    }
  };
}

const HocComponent = HOC(Example);

ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

運行如上代碼,就可以得到一個默認值爲 the initialValue of input 的 input 標籤。因爲 HOC 在 render 之前獲取了 WrappedComponent 的 Dom 結構,從而可以自定義一些自己的東西,然後再執行本身的渲染操作。

 

HOC 的功能雖然很強大,但是在使用過程中還是需要注意,React 官方給出了一些注意事項,在此不贅述~

 

附上具體示例代碼地址

 

mixin VS HOC

 

mixin 和 HOC 都能解決代碼複用的問題,但是 mixin 存在如下缺點:

 

  • 降低代碼的可讀性:組件的優勢在於將邏輯與是界面直接結合在一起,mixin 本質上會分散邏輯,理解起來難度大

  • mixin 會導致命名衝突:多個 mixin 和組件本身,方法名稱會有命名衝突風險,如果遇到了,不得不重命名某些方法

 

除了上面的顯著缺點外,還有一些其他的,詳見 Mixins Considered Harmful

 

而且 HOC 更接近於函數式變成的思想,在使用上也更加靈活,包括的功能點也更多。一張圖可以很形象地表達出兩者的區別:

 

由圖可以看出 mixin 是一種打補丁的做法,而 HOC 則是組件的嵌套,書寫起來也更加優雅~

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章