banner
SlhwSR

SlhwSR

热爱技术的一名全栈开发者
github
bilibili

hoc、hof實現及應用場景

一、是什麼#

高階函數(Higher-order function),至少滿足下列一個條件的函數

  • 接受一個或多個函數作為輸入
  • 輸出一個函數

React中,高階組件即接受一個或多個組件作為參數並且返回一個組件,本質也就是一個函數,並不是一個組件

const EnhancedComponent = highOrderComponent(WrappedComponent);

上述代碼中,該函數接受一個組件WrappedComponent作為參數,返回加工過的新組件EnhancedComponent

高階組件的這種實現方式,本質上是一個裝飾者設計模式

二、如何編寫#

最基本的高階組件的編寫模板如下:

import React, { Component } from 'react';

export default (WrappedComponent) => {
  return class EnhancedComponent extends Component {
    // do something
    render() {
      return <WrappedComponent />;
    }
  }
}

透過對傳入的原始組件 WrappedComponent 做一些你想要的操作(比如操作 props,提取 state,給原始組件包裹其他元素等),從而加工出想要的組件 EnhancedComponent

把通用的邏輯放在高階組件中,對組件實現一致的處理,從而實現代碼的重用

所以,高階組件的主要功能是封裝並分離組件的通用邏輯,讓通用邏輯在組件間更好地被重用

但在使用高階組件的同時,一般遵循一些約定,如下:

  • props 保持一致
  • 你不能在函數式(無狀態)組件上使用 ref 屬性,因為它沒有實例
  • 不要以任何方式改變原始組件 WrappedComponent
  • 透傳不相關 props 屬性給被包裹的組件 WrappedComponent
  • 不要再 render () 方法中使用高階組件
  • 使用 compose 組合高階組件
  • 包裝顯示名字以便於調試

這裡需要注意的是,高階組件可以傳遞所有的props,但是不能傳遞ref

如果向一個高階組件添加ref引用,那麼ref 指向的是最外層容器組件實例的,而不是被包裹的組件,如果需要傳遞refs的話,則使用React.forwardRef,如下:

function withLogging(WrappedComponent) {
    class Enhance extends WrappedComponent {
        componentWillReceiveProps() {
            console.log('Current props', this.props);
            console.log('Next props', nextProps);
        }
        render() {
            const {forwardedRef, ...rest} = this.props;
            // 把 forwardedRef 賦值給 ref
            return <WrappedComponent {...rest} ref={forwardedRef} />;
        }
    };

    // React.forwardRef 方法會傳入 props 和 ref 兩個參數給其回調函數
    // 所以這邊的 ref 是由 React.forwardRef 提供的
    function forwardRef(props, ref) {
        return <Enhance {...props} forwardRef={ref} />
    }

    return React.forwardRef(forwardRef);
}
const EnhancedComponent = withLogging(SomeComponent);

三、應用場景#

透過上面的了解,高階組件能夠提高代碼的重用性和靈活性,在實際應用中,常常用於與核心業務無關但又在多個模塊使用的功能,如權限控制、日誌記錄、數據校驗、異常處理、統計上報等

舉個例子,存在一個組件,需要從快取中獲取數據,然後渲染。一般情況,我們會如下編寫:

import React, { Component } from 'react'

class MyComponent extends Component {

  componentWillMount() {
      let data = localStorage.getItem('data');
      this.setState({data});
  }
  
  render() {
    return <div>{this.state.data}</div>
  }
}

上述代碼當然可以實現該功能,但是如果還有其他組件也有類似功能的時候,每個組件都需要重複寫componentWillMount中的代碼,這明顯是冗雜的

下面就可以透過高階組件來進行改寫,如下:

import React, { Component } from 'react'

function withPersistentData(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem('data');
        this.setState({data});
    }
    
    render() {
      // 透過{...this.props} 把傳遞給當前組件的屬性繼續傳遞給被包裝的組件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
}

const MyComponentWithPersistentData = withPersistentData(MyComponent2)

再比如組件渲染性能監控,如下:

class Home extends React.Component {
    render() {
        return (<h1>Hello World.</h1>);
    }
}
function withTiming(WrappedComponent) {
    return class extends WrappedComponent {
        constructor(props) {
            super(props);
            this.start = 0;
            this.end = 0;
        }
        componentWillMount() {
            super.componentWillMount && super.componentWillMount();
            this.start = Date.now();
        }
        componentDidMount() {
            super.componentDidMount && super.componentDidMount();
            this.end = Date.now();
            console.log(`${WrappedComponent.name} 組件渲染時間為 ${this.end - this.start} ms`);
        }
        render() {
            return super.render();
        }
    };
}

export default withTiming(Home);

參考文獻#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。