React 性能優化 —— 淺談 PureComponent 組件 與 memo 組件

在談性能優化以前,先拋出一個問題:html

一個 React 組件,它包含兩個子組件,分別是函數組件和 Class 組件。當這個 React 組件的 state 發生變化時,兩個子組件的 props 並無發生變化,此時是否會致使函數子組件和 Class 子組件發生重複渲染呢?前端

曾拿這個問題問過很多前端求職者,但不多能給出正確的答案。下面就這個問題,淺談下本身的認識。react

1、場景復現

針對上述問題,先進行一個簡單的復現驗證。git

App 組件包含兩個子組件,分別是函數組件 ChildFunc 和類組件 ChildClass。App 組件每隔 2 秒會對自身狀態 cnt 自行累加 1,用於驗證兩個子組件是否會發生重複渲染,具體代碼邏輯以下。github

App 組件:api

import React, { Component, Fragment } from 'react';
import ReactDOM from 'react-dom';
import ChildClass from './ChildClass.jsx';
import ChildFunc from './ChildFunc.jsx';

class App extends Component {
  state = {
    cnt: 1
  };

  componentDidMount() {
    setInterval(() => this.setState({ cnt: this.state.cnt + 1 }), 2000);
  }

  render() {
    return (
      <Fragment> <h2>疑問:</h2> <p> 一個 React 組件,它包含兩個子組件,分別是函數組件和 Class 組件。當這個 React 組件的 state 發生變化時,兩個子組件的 props 並無發生變化,此時是否會致使函數子組件和 Class 子組件發生重複渲染呢? </p> <div> <h3>驗證(性能優化前):</h3> <ChildFunc /> <ChildClass /> </div> </Fragment>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root')); 複製代碼

Class 組件:數組

import React, { Component } from 'react';

let cnt = 0;

class ChildClass extends Component {
  render() {
    cnt = cnt + 1;

    return <p>Class組件發生渲染次數: {cnt}</p>;
  }
}

export default ChildClass;
複製代碼

函數組件:性能優化

import React from 'react';

let cnt = 0;

const ChildFunc = () => {
  cnt = cnt + 1;

  return <p>函數組件發生渲染次數: {cnt}</p>;
};

export default ChildFunc;
複製代碼

實際驗證結果代表,以下圖所示,不管是函數組件仍是 Class 組件,只要父組件的 state 發生了變化,兩者均會產生重複渲染。數據結構

性能優化前

2、性能優化

那麼該如何減小子組件發生重複渲染呢?好在 React 官方提供了 memo 組件和PureComponent組件分別用於減小函數組件和類組件的重複渲染,具體優化邏輯以下:less

Class 組件:

import React, { PureComponent } from 'react';

let cnt = 0;

class ChildClass extends PureComponent {
  render() {
    cnt = cnt + 1;

    return <p>Class組件發生渲染次數: {cnt}</p>;
  }
}

export default ChildClass;
複製代碼

函數組件:

import React, { memo } from 'react';

let cnt = 0;

const OpChildFunc = () => {
  cnt = cnt + 1;

  return <p>函數組件發生渲染次數: {cnt}</p>;
};

export default memo(OpChildFunc);
複製代碼

實際驗證結果以下圖所示,每當 App 組件狀態發生變化時,優化後的函數子組件和類子組件均再也不產生重複渲染。

性能優化後

下面結合 React 源碼,淺談下 PureComponent 組件和 memo 組件的實現原理。

3、PureComponent 組件

3.1 PureComponent 概念

如下內容摘自React.PureComponent

React.PureComponentReact.Component 很類似。二者的區別在於 React.Component 並未實現 shouldComponentUpdate(),而 React.PureComponent 中以淺層對比 prop 和 state 的方式來實現了該函數。

若是賦予 React 組件相同的 props 和 state,render() 函數會渲染相同的內容,那麼在某些狀況下使用 React.PureComponent 可提升性能。

注意:

React.PureComponent 中的 shouldComponentUpdate() 僅做對象的淺層比較。若是對象中包含複雜的數據結構,則有可能由於沒法檢查深層的差異,產生錯誤的比對結果。僅在你的 props 和 state 較爲簡單時,才使用 React.PureComponent,或者在深層數據結構發生變化時調用 forceUpdate() 來確保組件被正確地更新。你也能夠考慮使用 immutable 對象加速嵌套數據的比較。

此外,React.PureComponent 中的 shouldComponentUpdate() 將跳過全部子組件樹的 prop 更新。所以,請確保全部子組件也都是「純」的組件。

3.2 PureComponent 性能優化實現機制

3.2.1 PureComponent 組件定義

咱們先看下在 React 中 PureComponent組件是如何定義的,如下代碼摘自 React v16.9.0 中的 ReactBaseClasses.js文件。

// ComponentDummy起橋接做用,用於PureComponent實現一個正確的原型鏈,其原型指向Component.prototype
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

// 定義PureComponent構造函數
function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

// 將PureComponent的原型指向一個新的對象,該對象的原型正好指向Component.prototype
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());

// 將PureComponent原型的構造函數修復爲PureComponent
pureComponentPrototype.constructor = PureComponent;

// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);

// 建立標識isPureReactComponent,用於標記是不是PureComponent
pureComponentPrototype.isPureReactComponent = true;
複製代碼

3.2.2 PureComponent 組件的性能優化實現機制

名詞解釋:

  • work-in-progress(簡寫 WIP: 半成品):表示還沒有完成的 Fiber,也就是還沒有返回的堆棧幀,對象 workInProgress 是 reconcile 過程當中從 Fiber 創建的當前進度快照,用於斷點恢復。

如下代碼摘自 React v16.9.0 中的 ReactFiberClassComponent.js文件。

function checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext, ) {
  const instance = workInProgress.stateNode;

  // 若是這個組件實例自定義了shouldComponentUpdate生命週期函數
  if (typeof instance.shouldComponentUpdate === 'function') {
    startPhaseTimer(workInProgress, 'shouldComponentUpdate');

    // 執行這個組件實例自定義的shouldComponentUpdate生命週期函數
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );
    stopPhaseTimer();

    return shouldUpdate;
  }

  // 判斷當前組件實例是不是PureReactComponent
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
     /** * 1. 淺比較判斷 oldProps 與newProps 是否相等; * 2. 淺比較判斷 oldState 與newState 是否相等; */
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  return true;
}
複製代碼

由上述代碼能夠看出,若是一個 PureComponent 組件自定義了shouldComponentUpdate生命週期函數,則該組件是否進行渲染取決於shouldComponentUpdate生命週期函數的執行結果,不會再進行額外的淺比較。若是未定義該生命週期函數,纔會淺比較狀態 state 和 props。

4、memo 組件

4.1 React.memo 概念

如下內容摘自React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});
複製代碼

React.memo高階組件。它與React.PureComponent很是類似,但它適用於函數組件,但不適用於 class 組件。

若是你的函數組件在給定相同props的狀況下渲染相同的結果,那麼你能夠經過將其包裝在React.memo中調用,以此經過記憶組件渲染結果的方式來提升組件的性能表現。這意味着在這種狀況下,React 將跳過渲染組件的操做並直接複用最近一次渲染的結果。

默認狀況下其只會對複雜對象作淺層對比,若是你想要控制對比過程,那麼請將自定義的比較函數經過第二個參數傳入來實現。

function MyComponent(props) {
  /* 使用 props 渲染 */
}

function areEqual(prevProps, nextProps) {
  /* 若是把 nextProps 傳入 render 方法的返回結果與 將 prevProps 傳入 render 方法的返回結果一致則返回 true, 不然返回 false */
}

export default React.memo(MyComponent, areEqual);
複製代碼

此方法僅做爲性能優化的方式而存在。但請不要依賴它來「阻止」渲染,由於這會產生 bug。

注意 與 class 組件中 shouldComponentUpdate() 方法不一樣的是,若是 props 相等,areEqual 會返回 true;若是 props 不相等,則返回 false。這與 shouldComponentUpdate 方法的返回值相反。

4.2 React.memo 性能優化實現機制

4.2.1 memo 函數定義

咱們先看下在 React 中 memo 函數是如何定義的,如下代碼摘自 React v16.9.0 中的memo.js文件。

export default function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  return {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
}
複製代碼

其中:

  • type:表示自定義的 React 組件;
  • compare:表示自定義的性能優化函數,相似shouldcomponentupdate生命週期函數;

4.2.2 memo 函數的性能優化實現機制

如下代碼摘自 React v16.9.0 中的 ReactFiberBeginWork.js文件。

function updateMemoComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, updateExpirationTime, renderExpirationTime: ExpirationTime, ): null | Fiber {

  /* ...省略...*/

  // 判斷更新的過時時間是否小於渲染的過時時間
  if (updateExpirationTime < renderExpirationTime) {
    const prevProps = currentChild.memoizedProps;

    // 若是自定義了compare函數,則採用自定義的compare函數,不然採用官方的shallowEqual(淺比較)函數。
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;

    /** * 1. 判斷當前 props 與 nextProps 是否相等; * 2. 判斷即將渲染組件的引用是否與workInProgress Fiber中的引用是否一致; * * 只有二者都爲真,纔會退出渲染。 */
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      // 若是都爲真,則退出渲染
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  }

  /* ...省略...*/
}
複製代碼

由上述代碼能夠看出,updateMemoComponent函數決定是否退出渲染取決於如下兩點:

  • 當前 props 與 nextProps 是否相等;
  • 即將渲染組件的引用是否與 workInProgress Fiber 中的引用是否一致;

只有兩者都爲真,纔會退出渲染。

其餘:

相關文章
相關標籤/搜索