React PureComponent 使用时父组件状态更新后子组件未更新

551 阅读5分钟

在之前使用 React 类组件开发微信小程序的过程中,遇到了父组件状态更新成功,但是它的子组件未更新的问题,找了一段时间的bug,发现问题和 React.PureComponent 的性能优化有关。

一、Component和PureComponent

这里我们主要看 React 的 PureComponent,下面是 PureComponent 的源码(React 18.2.0,下文的代码均与该版本一致)。

function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

/**
 * Convenience component with default shallow equality check for sCU.
 */
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;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;

从上面的源码中可以看出,PureComponent 间接地继承了 Component ,然后新增了一个 isPureReactComponent 属性,用来进行两者的区分。

React.PureComponent 和 React.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 更新。因此,请确保所有子组件也都是“纯”的组件。

二、checkShouldComponentUpdate

在类组件中,判断组件是否需要更新要用到一个叫做 shouldComponentUpdate 的方法,但在我们没有手动实现该方法时,组件是怎么判断是否需要更新的呢?其实,在 shouldComponentUpdate 发挥它的作用前,要先调用 checkShouldComponentUpdate 方法。如下图所示:

image.png

从源码中我们可以看到:

  • 当我们在组件中手动实现了 shouldComponentUpdate 方法的时候,checkShouldComponentUpdate 会将 shouldComponentUpdate 的执行结果作为函数的返回值。
  • 当我们没有手动实现 shouldComponentUpdate 的时候,它会通过 isPureReactComponent 属性判断组件是否为 PureComponent,如果是,就会对更新前后的 state 和 props 进行浅对比并返回对比结果,如果不是则返回 true,默认组件需要更新。

在项目开发中,我使用的恰巧是 PureComponent,同时将父组件中的复杂数据作为了子组件的props,父组件状态变化时,改变的是深层次的数据,而在使用 PureComponent 的情况下会进行浅对比,通过浅对比,错误地判断子组件不需要更新,然后就出现了父组件状态更新,但子组件未更新的问题。

为什么会出现这样的判断结果呢,让我们去探究一下浅对比的过程。

三、浅对比 shallowEqual

下面是 React 中浅对比的实现:

/**
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * Returns true when the values of all keys are strictly equal.
 */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

上述代码中的 is 即为 Object.is()方法,可参考MDN。
浅对比实现逻辑如下:

  1. 如果两者为同一对象,则返回 true;
  2. 两者中如果有一个不为 object 或者为 null ,则返回 false;
  3. 比较键的数量,若不相等则返回 false;
  4. 比较两者每个键的对应的值是否相同,若有不同则返回 false;(此处,若某个键的值为引用类型数据且其在内存中的首地址未改变,当它深层次的数据发生变化时,则该浅对比函数无法检测,我项目中遇到的 bug 也是由此产生的);

四、问题解决

现在产生 bug 的原因找到了,怎么来解决或者避免遇到这样的问题呢?
我个人想到的解决办法如下:

  1. 根据项目需求,综合性能等方面考虑,合理按需使用 PureComponent 和 Component,这里如果使用 Component 则不会出现上述问题。
  2. 在子组件中实现 shouldComponentUpdate 方法,手动对比更新前后的 props ,然后判断子组件是否需要更新。
  3. 在使用 PureComponent 时,通过给子组件添加一个动态变化的 key(这也是我在项目中使用的方法,该方法会略微降低性能,但其实际影响在我做的项目中可忽略),使得该子组件在每次 props 或者 state 改变时都会重新渲染。
    在key值的上选取需要适当,若是选择一个唯一固定值作为子组件 key 值(常用于列表),那么子组件 props 或者 state 更新前后 key 值不变,而通过浅对比以及 diff 比较之后判断组件不需要重新渲染,那么依旧会存在父组件更新子组件不更新的问题,而只有给组件一个随组件变化而更新的 key 值时,在组件更新前后进行 diff 比较时,才会判定组件需要更新,从而更新用户界面。