寫給本身的React HOC(高階組件)手冊

前言

HOC(高階組件)是React中的一種組織代碼的手段,而不是一個API. javascript

這種設計模式能夠複用在React組件中的代碼與邏輯,由於通常來說React組件比較容易複用渲染函數, 也就是主要負責HTML的輸出.java

高階組件其實是通過一個包裝函數返回的組件,這類函數接收React組件處理傳入的組件,而後返回一個新的組件.
注意:前提是創建在不修改原有組件的基礎上.設計模式

文字描述太模糊,藉助於官方文檔稍稍修改,咱們能夠更加輕鬆的理解高階組件.數組

具體的實施

流程以下:性能優化

  1. 找出組件中複用的邏輯
  2. 建立適用於上方邏輯的函數
  3. 利用這個函數來建立一個組件
  4. enjoy it

找出組件中複用的邏輯

在實際開發中, 這種邏輯的組件很是常見:服務器

  1. 組件建立
  2. 向服務器拉取數據
  3. 利用數據渲染組件
  4. 監聽數據的變化
  5. 數據變化或者觸發修改的事件
  6. 利用變化後的數據再次渲染
  7. 組件銷燬移除監聽的數據源

首先咱們來建立一個生產假數據的對象來模擬數據源:app

const fakeDataGenerator = ()=>({
  timer: undefined,
  getData(){
    return ['hello', 'world'];
  },
  addChangeListener(handleChangeFun){ // 監聽數據產生鉤子

    if(this.timer){
      return;
    }

    this.timer = setInterval(()=> {
      handleChangeFun();
    },2000)
  },
  removeChangeListener(){ // 中止數據監聽
    clearInterval(this.timer);
  }
});

而後來編寫咱們的組件A:函數

const FakeDataForA = fakeDataGenerator();

class A extends React.Component {

  constructor(props) {// 1 組件建立
    super(props);

    this.state = {
      someData: fakeData.getData() // 1.1 向服務器拉取數據
    }

  }

  handleFakeDataChange = ()=>{ 
    this.setState({
      someData:fakeData.getData() // 4. 數據變化或者觸發修改的事件
    });
  }

  componentDidMount(){
    // 3. 監聽數據的變化
    // 4. 數據變化或者觸發修改的事件
    fakeData.addChangeListener(this.handleFakeDataChange); 
  }

  componentWillUnmount(){
    fakeData.removeChangeListener(); // 6. 組件銷燬移除監聽的數據源
  }

  render() {
    return (
      {/*
        2. 利用數據渲染組件
        5. 利用變化後的數據再次渲染
      */}
      this.state.someData.map(name => (<span key={name}>{name}</span>)) 
    )
  }
}

ReactDOM.render(<A />, document.getElementById('root'));

而後咱們再來建立一個組件B這個雖然渲染方式不一樣,可是數據獲取的邏輯是一致的.
在通常的開發過程當中實際上也是遵循這個請求模式的,而後建立一個組件B:性能

const FakeDataForB = fakeDataGenerator();

class B extends React.Component {

  constructor(props) {// 1 組件建立
    super(props);

    this.state = {
      someData: fakeData.getData() // 1.1 向服務器拉取數據
    }

  }

  handleFakeDataChange = ()=>{ 
    this.setState({
      someData:fakeData.getData() // 4. 數據變化或者觸發修改的事件
    });
  }

  componentDidMount(){
    // 3. 監聽數據的變化
    // 4. 數據變化或者觸發修改的事件
    fakeData.addChangeListener(this.handleFakeDataChange); 
  }

  componentWillUnmount(){
    fakeData.removeChangeListener(); // 6. 組件銷燬移除監聽的數據源
  }

  render() {
    return (
      {/*
        2. 利用數據渲染組件
        5. 利用變化後的數據再次渲染
      */}
      this.state.someData.map(name => (<div key={name}>{name}</div>)) 
    )
  }
}

ReactDOM.render(<B />, document.getElementById('root'));

這裏我把redner中原來渲染的span標籤改成了div標籤,雖然這是一個小小的變化可是請你腦補這是兩個渲染結果徹底不一樣的組件好了. 優化

這時候問題以及十分明顯了組件A和B明顯有大量的重複邏輯可是藉助於React組件卻沒法將這公用的邏輯來抽離.

在通常的開發中沒有這麼完美重複的邏輯代碼,例如在生命週期函數中B組件可能多了幾個操做或者A組件數據源獲取的地址不一樣.
可是這裏依然存在大量的能夠被複用的邏輯.

一個返回組件的函數

這種函數的第一個參數接收一個React組件,而後返回這個組件:

function MyHoc(Wrap) {
  return class extends React.Component{
    render(){
      <Wrap ></Wrap>
    }
  }
}

就目前來講這個函數沒有任何實際功能只是將原有的組件包裝返回而已.

可是若是咱們將組件A和B傳入到這個函數中,而使用返回的函數,咱們能夠獲得了什麼.
咱們獲取了在原有的組件上的一層包裝,利用這層包裝咱們能夠把組件A和B的共同邏輯提取到這層包裝上.

咱們來刪除組件A和B有關數據獲取以及修改的操做:

class A extends React.Component {

  componentDidMount(){
    // 這裏執行某些操做 假設和另一個組件不一樣
  }

  componentWillUnmount(){
    // 這裏執行某些操做 假設和另一個組件不一樣
  }

  render() {
    return (
      this.state.data.map(name => (<span key={name}>{name}</span>))
    )
  }
}

class B extends React.Component {

  componentDidMount(){
    // 這裏執行某些操做 假設和另一個組件不一樣
  }

  componentWillUnmount(){
    // 這裏執行某些操做 假設和另一個組件不一樣
  }

  render() {
    return (
      this.state.data.map(name => (<div key={name}>{name}</div>))
    )
  }
}

而後將在這層包裝上的獲取到的外部數據使用props來傳遞到原有的組件中:

function MyHoc(Wrap) {
  return class extends React.Component{

    constructor(props){

      super(props);

      this.state = {
        data:fakeData // 假設這樣就獲取到了數據, 先不考慮其餘狀況
      }

    }

    render(){
      return <Wrap data={this.state.data} {...this.props}></Wrap> {/* 經過 props 把獲取到的數據傳入 */}
    }
  }
}

在這裏咱們在 HOC 返回的組件中獲取數據, 而後把數據傳入到內部的組件中, 那麼數據獲取的這種功能就被單獨的拿了出來.
這樣組件A和B只要關注本身的 props.data 就能夠了徹底不須要考慮數據獲取和自身的狀態修改.

可是咱們注意到了組件A和B原有獲取數據源不一樣,咱們如何在包裝函數中處理?

這點好解決,利用函數的參數差別來抹消掉返回的高階組件的差別.

既然A組件和B組件的數據源不一樣那麼這個函數就另外接收一個數據源做爲參數好了.
而且咱們將以前通用的邏輯放到了這個內部的組件上:

function MyHoc(Wrap,fakeData) { // 此次咱們接收一個數據源
  return class extends React.Component{

    constructor(props){

      super(props);
      this.state = {
        data: fakeData.getData() // 模擬數據獲取
      }
      
    }

    handleDataChange = ()=>{
      this.setState({
        data:fakeData.getData()
      });
    }

    componentDidMount() {
      fakeData.addChangeListener(this.handleDataChange);
    }

    componentWillUnmount(){
      fakeData.removeChangeListener();
    }

    render(){
      <Wrap data={this.state.data} {...this.props}></Wrap>
    }
  }
}

利用高階組件來建立組件

通過上面的思考,實際上已經完成了99%的工做了,接下來就是完成剩下的1%,把它們組合起來.

僞代碼:

const
  FakeDataForA = FakeDataForAGenerator(),
  FakeDataForB = FakeDataForAGenerator(); // 兩個不一樣的數據源

function(Wrap,fakdData){ // 一個 HOC 函數
  return class extends React.Components{};
}

class A {}; // 兩個不一樣的組件
class B {}; // 兩個不一樣的組件

const 
  AFromHoc = MyHoc(A,FakeDataForA),
  BFromHoc = MyHoc(B,FakeDataForB); // 分別把不一樣的數據源傳入, 模擬者兩個組件須要不一樣的數據源, 可是獲取數據邏輯一致

這個時候你就能夠渲染本身的高階組件AFromHocBFromHoc了.
這兩個組件使用不一樣的數據源來獲取數據,通用的部分已經被抽離.

函數約定

HOC函數不要將多餘的props傳遞給被包裹的組件

HOC函數須要像透明的同樣,通過他的包裝產生的新的組件和傳入前沒有什麼區別.
這樣作的目的在於,咱們不須要考慮通過HOC函數後的組件會產生什麼變化而帶來額外的心智負擔.
若是你的HOC函數對傳入的組件進行了修改,那麼套用這種HOC函數屢次後返回的組件在使用的時候.
你不得不考慮這個組件帶來的一些非預期行爲.

因此請不要將本來組件不須要的props傳入:

render() {
  // 過濾掉非此 HOC 額外的 props,且不要進行透傳
  const { extraProp, ...passThroughProps } = this.props;

  // 將 props 注入到被包裝的組件中。
  // 一般爲 state 的值或者實例方法。
  const injectedProp = someStateOrInstanceMethod;

  // 將 props 傳遞給被包裝組件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

HOC是函數!利用函數來最大化組合性

由於HOC是一個返回組件的函數,只要是函數能夠作的事情HOC一樣能夠作到.
利用這一點,咱們能夠借用在使用React以前咱們就已經學會的一些東西.

例如定義一個高階函數用於返回一個高階組件:

function HighLevelHoc(content) {
  return function (Wrap, className) {
    return class extends React.Component {
      render() {
        return (
          <Wrap {...this.props} className={className} >{content}</Wrap>
        )
      }
    }
  }
}

class Test extends React.Component {
  render() {
    return (
      <p>{this.props.children || 'hello world'}</p>
    )
  }
}

const H1Test = HighLevelHoc('foobar')(Test, 1);


ReactDOM.render(<H1Test />, document.getElementById('root'));

或者乾脆是一個不接收任何參數的函數:

function DemoHoc(Wrap) { // 用於向 Wrap 傳入一個固定的字符串
  return class extends React.Component{
    render(){
      return (
        <Wrap {...this.props}>{'hello world'}</Wrap>
      )
    }
  } 
}

function Demo(props) {
  return (
    <div>{props.children}</div>
  )
}

const App = DemoHoc(Demo);

ReactDOM.render(<App />, document.getElementById('root'));

注意

不要在 render 方法中使用 HOC

咱們都知道 React 會調用 render 方法來渲染組件, 固然 React 也會作一些額外的工做例如性能優化.
在組件從新渲染的時候 React 會判斷當前 render 返回的組件和未以前的組件是否相等 === 若是相等 React 會遞歸更新組件, 反之他會完全的卸載以前的舊的版原本渲染當前的組件.

HOC每次返回的內容都是一個新的內容:

function Hoc(){
  return {}
}
console.log( Hoc()===Hoc() ) // false

若是在 render 方法中使用:

render() {
  const DemoHoc = Hoc(MyComponent); // 每次調用 render 都會返回一個新的對象
  // 這將致使子樹每次渲染都會進行卸載,和從新掛載的操做!
  return <DemoHoc />;
}

記得複製靜態方法

React 的組件通常是繼承 React.Component 的子類.
不要忘記了一個類上除了實例方法外還有靜態方法, 使用 HOC 咱們對組件進行了一層包裝會覆蓋掉原來的靜態方法:

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

Demo.echo = function () {
  console.log('hello world');
}

Demo.echo();// 是能夠調用的

// -------- 定一個類提供一個靜態方法

function DemoHoc(Wrap) {
  return class extends React.Component{
    render(){
      return (
        <Wrap>{'hello world'}</Wrap>
      )
    }
  } 
}

const App = DemoHoc(Demo);

// ----- HOC包裝這個類

App.echo(); // error 這個靜態方法不見了

解決方式

在 HOC 內部直接將原來組件的靜態方法複製就能夠了:

function DemoHoc(Wrap) {

  const myClass = class extends React.Component{
    render(){
      return (
        <Wrap>{'hello world'}</Wrap>
      )
    }
  }

  myClass.echo = Wrap.echo;

  return myClass;
}

不過這樣一來 HOC 中就須要知道被複制的靜態方法名是什麼, 結合以前提到的靈活使用 HOC 咱們可讓 HOC 接收靜態方法參數名稱:

function DemoHoc(Wrap,staticMethods=[]) { // 默認空數組

  const myClass = class extends React.Component{
    render(){
      return (
        <Wrap>{'hello world'}</Wrap>
      )
    }
  }

  for (const methodName of staticMethods) { // 循環複製
    myClass[methodName] = Wrap[methodName];
  }

  return myClass;
}

// -----
const App = DemoHoc(Demo,['echo']);

此外通常咱們編寫組件的時候都是一個文件對應一個組件, 這時候咱們能夠把靜態方法導出.
HOC 不拷貝靜態方法, 而是須要這些靜態方法的組件直接引入就行了:

來自官方文檔
// 使用這種方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...單獨導出該方法...
export { someFunction };

// ...並在要使用的組件中,import 它們
import MyComponent, { someFunction } from './MyComponent.js';

透傳 ref

ref 做爲組件上的特殊屬性, 沒法像普通的 props 那樣被向下傳遞.

例如咱們有一個組件, 咱們想使用 ref 來引用這個組件而且試圖調用它的 echo 方法:

class Wraped extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      message:''
    }
  }

  echo(){
    this.setState({
      message:'hello world'
    });
  }

  render(){
    return <div>{this.state.message}</div>
  }
}

咱們使用一個 HOC 包裹它:

function ExampleHoc(Wrap) {
  return class extends React.Component{
    render(){
      return <Wrap></Wrap>
    }
  }
}

const Example = ExampleHoc(Wraped);
// 獲得了一個高階組件

如今咱們把這個組件放入到 APP 組件中進行渲染, 而且使用 ref 來引用這個返回的組件, 而且試圖調用它的 echo 方法:

const ref = React.createRef();

class App extends React.Component {

  handleEcho = () => {
    ref.current.echo();
  }

  render() {
    return (
      <div>
        <Example ref={ref}></Example>
        <button onClick={this.handleEcho}>echo</button> {/* 點擊按鈕至關於執行echo */}
      </div>
    )
  }
}

可是當你點擊按鈕試圖觸發子組件的事件的時候它不會起做用, 系統報錯沒有 echo 方法.

實際上 ref 被綁定到了 HOC 返回的那個匿名類上, 想要綁定到內部的組件中咱們能夠進行 ref 透傳.
默認的狀況下 ref 是沒法被進行向下傳遞的由於 ref 是特殊的屬性就和 key 同樣不會被添加到 props 中, 所以 React 提供了一個 API 來實現透傳 ref 的這種需求.

這個 API 就是 React.forwardRef.

這個方法接收一個函數返回一個組件, 在這個含中它能夠讀取到組件傳入的 ref , 某種意義上 React.forwardRef 也至關於一個高階組件:

const ReturnedCompoent = React.forwardRef((props, ref) => {
  // 咱們能夠獲取到在props中沒法獲取的 ref 屬性了
  return // 返回這個須要使用 ref 屬性的組件
});

咱們把這個 API 用在以前的 HOC 中:

function ExampleHoc(Wrap) {
  class Inner extends React.Component {
    render() {
      const { forwardedRef,...rest} = this.props;
      return <Wrap ref={forwardedRef} {...rest} ></Wrap> // 2. 咱們接收到 props 中被更名的 ref 而後綁定到 ref 上
    }
  }
  return React.forwardRef((props,ref)=>{ // 1. 咱們接收到 ref 而後給他更名成 forwardedRef 傳入到props中
    return <Inner {...props} forwardedRef={ref} ></Inner>
  })
}

這個時候在調用 echo 就沒有問題了:

handleEcho = () => {
  ref.current.echo();
}
相關文章
相關標籤/搜索