在之前使用 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 方法。如下图所示:
从源码中我们可以看到:
- 当我们在组件中手动实现了 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。
浅对比实现逻辑如下:
- 如果两者为同一对象,则返回 true;
- 两者中如果有一个不为 object 或者为 null ,则返回 false;
- 比较键的数量,若不相等则返回 false;
- 比较两者每个键的对应的值是否相同,若有不同则返回 false;(此处,若某个键的值为引用类型数据且其在内存中的首地址未改变,当它深层次的数据发生变化时,则该浅对比函数无法检测,我项目中遇到的 bug 也是由此产生的);
四、问题解决
现在产生 bug 的原因找到了,怎么来解决或者避免遇到这样的问题呢?
我个人想到的解决办法如下:
- 根据项目需求,综合性能等方面考虑,合理按需使用 PureComponent 和 Component,这里如果使用 Component 则不会出现上述问题。
- 在子组件中实现 shouldComponentUpdate 方法,手动对比更新前后的 props ,然后判断子组件是否需要更新。
- 在使用 PureComponent 时,通过给子组件添加一个动态变化的 key(这也是我在项目中使用的方法,该方法会略微降低性能,但其实际影响在我做的项目中可忽略),使得该子组件在每次 props 或者 state 改变时都会重新渲染。
在key值的上选取需要适当,若是选择一个唯一固定值作为子组件 key 值(常用于列表),那么子组件 props 或者 state 更新前后 key 值不变,而通过浅对比以及 diff 比较之后判断组件不需要重新渲染,那么依旧会存在父组件更新子组件不更新的问题,而只有给组件一个随组件变化而更新的 key 值时,在组件更新前后进行 diff 比较时,才会判定组件需要更新,从而更新用户界面。