React 之 高階組件的理解


1.基本概念

高階組件是參數為組件,返回值為新組件的函數。

 

2.舉例說明

① 裝飾工廠模式

組件是 react 中的基本單元,組件中通常有一些邏輯(非渲染)需要復用處理。這里我們可以用高階組件對組件內部中的一些通用進行封裝。

未封裝時,相同的邏輯無法復用:

渲染評論列表

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // 假設 "DataSource" 是個全局范圍內的數據源變量
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // 訂閱更改
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除訂閱
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 當數據源更新時,更新組件狀態
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

渲染博客列表

lass BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

借用高階組件,封裝公用邏輯:

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

組件加工(加工:處理公用邏輯)工廠,接受舊組件,返回新組件:

// 此函數接收一個組件...
function withSubscription(WrappedComponent, selectData) {
  // ...並返回另一個組件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...負責訂閱相關的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... 並使用新數據渲染被包裝的組件!
      // 請注意,我們可能還會傳遞其他屬性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

 

② 高階組件柯里化

高階組件是一個參數是組件返回值也是組件的函數,那么我們借助函數柯里化,確保最終返回的函數是高階組件就可以了。

import React, { Component } from "react";

const isEmpty = prop =>
  (prop && prop.hasOwnProperty("length") && prop.length === 0) ||
  (prop.constructor === Object && Object.keys(prop).length === 0);

export default loadingProp => WrappedComponent => {
  const hocComponent = class extends Component {
    componentDidMount() {
      this.startTimer = Date.now();
    }

    componentWillUpdate(nextProps, nextState) {
      console.log(nextProps)
      if (!isEmpty(nextProps[loadingProp])) {
        this.endTimer = Date.now();
      }
    }

    render() {
      const myProps = {
        loadingTime: ((this.endTimer - this.startTimer) / 1000).toFixed(2)
      };

      return isEmpty(this.props[loadingProp]) ? (
        <div>loading...</div>
      ) : (
        <WrappedComponent {...this.props} {...myProps} />
      );
    }
  };

  return hocComponent;
};

 

其中 ,而 loadingProp => ... 是一個返回值為高階組件的函數,其返回結果 WrappedComponent => {...}  是一個單參數(即被包裹的組件)的高階組件。這樣做有什么好處呢?為什么不寫一個高階組件並將參數與包裝組件一並傳遞過去?高階組件受限於返回值必須是組件,因此它無法柯里化。而在高階組件之上再構建一個函數就能進行柯里化。同時,返回的結果是單參數(被包裹組件)的高階組件可以直接做 hoc 嵌套,即 一個 hoc 嵌套 另一個 hoc(因為傳入值、傳出值都是 組件)。此外的對於 hoc 嵌套調用,可以借助 compose 工具函數 進行扁平化處理。

許多第三方庫提供都提供了 compose 工具函數,包括 lodash (比如 lodash.flowRight), Redux 和 Ramda

 

 ③ 設定 hoc 的顯示名稱 displayName

為了方便調試,設定 hoc 的顯示名稱類似: 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';
}

 

 3. 注意事項

 ① 不要在 render 方法中使用 hoc

因為  hoc 會產生新的組件,而 redner 方法經常被調用,所以會不斷產生新的組件(而在 react 的 diff 算法中會將之前舊的卸載而替換成新的,這不僅僅會對性能造成影響,同時重新掛載組件會導致該組件及其所有子組件的狀態丟失。

 

②務必復制靜態方法

手動一個個復制

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必須准確知道應該拷貝哪些方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

使用 hoist-non-react-statics 自動拷貝所有非 React 靜態方法

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}

  

③ Ref 需要轉發

雖然高階組件的約定是將所有 props 傳遞給被包裝組件,但這對於 refs 並不適用。那是因為 ref 實際上並不是一個 prop - 就像 key 一樣,它是由 React 專門處理的。如果將 ref 添加到 HOC 的返回組件中,則 ref 引用指向容器組件,而不是被包裝組件。

使用 hoc 包裹組件

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// 我們導出 LogProps,而不是 FancyButton。
// 雖然它也會渲染一個 FancyButton。
export default logProps(FancyButton);

形式上導入的是原組件,實際上導入的是 hoc 包裹的原組件。這時如果直接傳 ref 到該組件,實際上  ref 並沒有傳遞到原組件中,而停留在 hoc 組件上。

import FancyButton from './FancyButton';

const ref = React.createRef();

// 我們導入的 FancyButton 組件是高階組件(HOC)LogProps。
// 盡管渲染結果將是一樣的,
// 但我們的 ref 將指向 LogProps 而不是內部的 FancyButton 組件!
// 這意味着我們不能調用例如 ref.current.focus() 這樣的方法
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

這里我們需要對停留在 hoc 組件中的 ref 進行轉發,使其傳遞到原組件中

return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });

 

 

233


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM