React 性能优化 —— 浅谈 PureComponent 组件 与 memo 组件

在谈性能优化之前,先抛出一个问题:

一个 React 组件,它包含两个子组件,分别是函数组件和 Class 组件。当这个 React 组件的 state 发生变化时,两个子组件的 props 并没有发生变化,此时是否会导致函数子组件和 Class 子组件发生重复渲染呢?

曾拿这个问题问过不少前端求职者,但很少能给出正确的答案。下面就这个问题,浅谈下自己的认识。

一、场景复现

针对上述问题,先进行一个简单的复现验证。

App 组件包含两个子组件,分别是函数组件 ChildFunc 和类组件 ChildClass。App 组件每隔 2 秒会对自身状态 cnt 自行累加 1,用于验证两个子组件是否会发生重复渲染,具体代码逻辑如下。

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>疑问:</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 发生了变化,二者均会产生重复渲染。

ParentBeforeOptimization.gif

二、性能优化

那么该如何减少子组件发生重复渲染呢?好在 React 官方提供了 memo 组件和PureComponent组件分别用于减少函数组件和类组件的重复渲染,具体优化逻辑如下:

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 组件状态发生变化时,优化后的函数子组件和类子组件均不再产生重复渲染。

ParentAfterOptimization.gif

下面结合 React 源码,浅谈下 PureComponent 组件和 memo 组件的实现原理。

三、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 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。

四、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 中的引用是否一致;

只有二者都为真,才会退出渲染。

其他:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容