React 性能優(yōu)化 —— 淺談 PureComponent 組件 與 memo 組件

在談性能優(yōu)化之前,先拋出一個(gè)問(wèn)題:

一個(gè) React 組件,它包含兩個(gè)子組件,分別是函數(shù)組件和 Class 組件。當(dāng)這個(gè) React 組件的 state 發(fā)生變化時(shí),兩個(gè)子組件的 props 并沒(méi)有發(fā)生變化,此時(shí)是否會(huì)導(dǎo)致函數(shù)子組件和 Class 子組件發(fā)生重復(fù)渲染呢?

曾拿這個(gè)問(wèn)題問(wèn)過(guò)不少前端求職者,但很少能給出正確的答案。下面就這個(gè)問(wèn)題,淺談下自己的認(rèn)識(shí)。

一、場(chǎng)景復(fù)現(xiàn)

針對(duì)上述問(wèn)題,先進(jìn)行一個(gè)簡(jiǎn)單的復(fù)現(xiàn)驗(yàn)證。

App 組件包含兩個(gè)子組件,分別是函數(shù)組件 ChildFunc 和類(lèi)組件 ChildClass。App 組件每隔 2 秒會(huì)對(duì)自身狀態(tài) cnt 自行累加 1,用于驗(yàn)證兩個(gè)子組件是否會(huì)發(fā)生重復(fù)渲染,具體代碼邏輯如下。

App 組件:

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>疑問(wèn):</h2>
        <p>
          一個(gè) React 組件,它包含兩個(gè)子組件,分別是函數(shù)組件和 Class 組件。當(dāng)這個(gè) React 組件的 state
          發(fā)生變化時(shí),兩個(gè)子組件的 props 并沒(méi)有發(fā)生變化,此時(shí)是否會(huì)導(dǎo)致函數(shù)子組件和 Class 子組件發(fā)生重復(fù)渲染呢?
        </p>
        <div>
          <h3>驗(yàn)證(性能優(yōu)化前):</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組件發(fā)生渲染次數(shù): {cnt}</p>;
  }
}

export default ChildClass;

函數(shù)組件:

import React from 'react';

let cnt = 0;

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

  return <p>函數(shù)組件發(fā)生渲染次數(shù): {cnt}</p>;
};

export default ChildFunc;

實(shí)際驗(yàn)證結(jié)果表明,如下圖所示,無(wú)論是函數(shù)組件還是 Class 組件,只要父組件的 state 發(fā)生了變化,二者均會(huì)產(chǎn)生重復(fù)渲染。

ParentBeforeOptimization.gif

二、性能優(yōu)化

那么該如何減少子組件發(fā)生重復(fù)渲染呢?好在 React 官方提供了 memo 組件和PureComponent組件分別用于減少函數(shù)組件和類(lèi)組件的重復(fù)渲染,具體優(yōu)化邏輯如下:

Class 組件:

import React, { PureComponent } from 'react';

let cnt = 0;

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

    return <p>Class組件發(fā)生渲染次數(shù): {cnt}</p>;
  }
}

export default ChildClass;

函數(shù)組件:

import React, { memo } from 'react';

let cnt = 0;

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

  return <p>函數(shù)組件發(fā)生渲染次數(shù): {cnt}</p>;
};

export default memo(OpChildFunc);

實(shí)際驗(yàn)證結(jié)果如下圖所示,每當(dāng) App 組件狀態(tài)發(fā)生變化時(shí),優(yōu)化后的函數(shù)子組件和類(lèi)子組件均不再產(chǎn)生重復(fù)渲染。

ParentAfterOptimization.gif

下面結(jié)合 React 源碼,淺談下 PureComponent 組件和 memo 組件的實(shí)現(xiàn)原理。

三、PureComponent 組件

3.1 PureComponent 概念

以下內(nèi)容摘自React.PureComponent

React.PureComponentReact.Component 很相似。兩者的區(qū)別在于 React.Component 并未實(shí)現(xiàn) shouldComponentUpdate(),而 React.PureComponent 中以淺層對(duì)比 prop 和 state 的方式來(lái)實(shí)現(xiàn)了該函數(shù)。

如果賦予 React 組件相同的 props 和 state,render() 函數(shù)會(huì)渲染相同的內(nèi)容,那么在某些情況下使用 React.PureComponent 可提高性能。

注意:

React.PureComponent 中的 shouldComponentUpdate() 僅作對(duì)象的淺層比較。如果對(duì)象中包含復(fù)雜的數(shù)據(jù)結(jié)構(gòu),則有可能因?yàn)闊o(wú)法檢查深層的差別,產(chǎn)生錯(cuò)誤的比對(duì)結(jié)果。僅在你的 props 和 state 較為簡(jiǎn)單時(shí),才使用 React.PureComponent,或者在深層數(shù)據(jù)結(jié)構(gòu)發(fā)生變化時(shí)調(diào)用 forceUpdate() 來(lái)確保組件被正確地更新。你也可以考慮使用 immutable 對(duì)象加速嵌套數(shù)據(jù)的比較。

此外,React.PureComponent 中的 shouldComponentUpdate() 將跳過(guò)所有子組件樹(shù)的 prop 更新。因此,請(qǐng)確保所有子組件也都是“純”的組件。

3.2 PureComponent 性能優(yōu)化實(shí)現(xiàn)機(jī)制

3.2.1 PureComponent 組件定義

以下代碼摘自 React v16.9.0 中的 ReactBaseClasses.js文件。

// ComponentDummy起橋接作用,用于PureComponent實(shí)現(xiàn)一個(gè)正確的原型鏈,其原型指向Component.prototype
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

// 定義PureComponent構(gòu)造函數(shù)
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的原型指向一個(gè)新的對(duì)象,該對(duì)象的原型正好指向Component.prototype
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());

// 將PureComponent原型的構(gòu)造函數(shù)修復(fù)為PureComponent
pureComponentPrototype.constructor = PureComponent;

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

// 創(chuàng)建標(biāo)識(shí)isPureReactComponent,用于標(biāo)記是否是PureComponent
pureComponentPrototype.isPureReactComponent = true;

3.2.2 PureComponent 組件的性能優(yōu)化實(shí)現(xiàn)機(jī)制

名詞解釋?zhuān)?/strong>

  • work-in-progress(簡(jiǎn)寫(xiě) WIP: 半成品):表示尚未完成的 Fiber,也就是尚未返回的堆棧幀,對(duì)象 workInProgress 是 reconcile 過(guò)程中從 Fiber 建立的當(dāng)前進(jìn)度快照,用于斷點(diǎn)恢復(fù)。

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

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

  // 如果這個(gè)組件實(shí)例自定義了shouldComponentUpdate生命周期函數(shù)
  if (typeof instance.shouldComponentUpdate === 'function') {
    startPhaseTimer(workInProgress, 'shouldComponentUpdate');

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

    return shouldUpdate;
  }

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

  return true;
}

由上述代碼可以看出,如果一個(gè) PureComponent 組件自定義了shouldComponentUpdate生命周期函數(shù),則該組件是否進(jìn)行渲染取決于shouldComponentUpdate生命周期函數(shù)的執(zhí)行結(jié)果,不會(huì)再進(jìn)行額外的淺比較。如果未定義該生命周期函數(shù),才會(huì)淺比較狀態(tài) state 和 props。

四、memo 組件

4.1 React.memo 概念

以下內(nèi)容摘自React.memo

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

React.memo高階組件。它與React.PureComponent非常相似,但它適用于函數(shù)組件,但不適用于 class 組件。

如果你的函數(shù)組件在給定相同props的情況下渲染相同的結(jié)果,那么你可以通過(guò)將其包裝在React.memo中調(diào)用,以此通過(guò)記憶組件渲染結(jié)果的方式來(lái)提高組件的性能表現(xiàn)。這意味著在這種情況下,React 將跳過(guò)渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果。

默認(rèn)情況下其只會(huì)對(duì)復(fù)雜對(duì)象做淺層對(duì)比,如果你想要控制對(duì)比過(guò)程,那么請(qǐng)將自定義的比較函數(shù)通過(guò)第二個(gè)參數(shù)傳入來(lái)實(shí)現(xiàn)。

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

function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 傳入 render 方法的返回結(jié)果與
  將 prevProps 傳入 render 方法的返回結(jié)果一致則返回 true,
  否則返回 false
  */
}

export default React.memo(MyComponent, areEqual);

此方法僅作為性能優(yōu)化的方式而存在。但請(qǐng)不要依賴(lài)它來(lái)“阻止”渲染,因?yàn)檫@會(huì)產(chǎn)生 bug。

注意
與 class 組件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 會(huì)返回 true;如果 props 不相等,則返回 false。這與 shouldComponentUpdate 方法的返回值相反。

4.2 React.memo 性能優(yōu)化實(shí)現(xiàn)機(jī)制

4.2.1 memo 函數(shù)定義

我們先看下在 React 中 memo 函數(shù)是如何定義的,以下代碼摘自 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:表示自定義的性能優(yōu)化函數(shù),類(lèi)似shouldcomponentupdate生命周期函數(shù);

4.2.2 memo 函數(shù)的性能優(yōu)化實(shí)現(xiàn)機(jī)制

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

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

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

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

    // 如果自定義了compare函數(shù),則采用自定義的compare函數(shù),否則采用官方的shallowEqual(淺比較)函數(shù)。
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;

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

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

由上述代碼可以看出,updateMemoComponent函數(shù)決定是否退出渲染取決于以下兩點(diǎn):

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

只有二者都為真,才會(huì)退出渲染。

其他:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容