在React中,通常是通过在生命周期函数 shouldComponentUpdate 中来判断是否需要重新渲染组件。而在 PureComponent 组件中,会自动检测 props 和 state 是否发生变化来决定是否重新渲染组件。在这个过程中,对 props 和 state 就执行了浅比较(shallowEqual)判断。
shallowEqual
React 源码中,checkShouldComponentUpdate 函数中有这样一段代码:
// React 版本:16.13.1
// react-reconciler/src/ReactFiberClassComponent.old.js
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
可以看到,PureComponent 通过 shallowEqual 方法对 props 和 state 的前后状态做了一个浅比较。
我们先来看一下 shallowEqual 方法的定义:
// React 版本:16.13.1
// shared/shallowEqual.js
import is from './objectIs';
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* 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++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
export default shallowEqual;
在 shallowEqual 函数中,仅仅是用 Object.is() 对Object ( props 或 state ) 的 value 做了一个基本数据类型的比较。
下面,我们深入剖析一下 shallowEqual 函数
Object.is()
在 shallowEqual 函数中,首先调用了 is() 对基本数据类型做了比较,is() 方法的实现在 objectIs.js 文件中:
// React 版本:16.13.1
// shared/objectIs.js
/**
* inlined Object.is polyfill to avoid requiring consumers ship their own
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs: (x: any, y: any) => boolean =
typeof Object.is === 'function' ? Object.is : is;
export default objectIs;
在 is() 方法中:
(x === y && (x !== 0 || 1 / x === 1 / y))是处理+0 != -0的情况(x !== x && y !== y)是处理NaN === NaN的情况
is() 方法其实就是 Object.is() 的 Polyfill:
if (!Object.is) {
Object.is = function(x, y) {
// SameValue algorithm
if (x === y) { // Steps 1-5, 7-10
// Steps 6.b-6.e: +0 != -0
return x !== 0 || 1 / x === 1 / y;
} else {
// Step 6.a: NaN == NaN
return x !== x && y !== y;
}
};
}
在使用 === 比较时,有两种情况不符合我们的期望:
+0 === -0 // true,我们期望它返回false
NaN === NaN // false,我们期望它返回true
Object.is 就修复了这两种不符合预期的情况,使得 Object.is() 总是返回我们需要的结果:
Object.is(NaN, NaN) // true
Object.is(-0, +0) // false
下面6种情况,Object.is() 会返回true
- 两个值都是 undefined
- 两个值都是 null
- 两个值都是 true 或者都是 false
- 两个值是由相同个数的字符按照相同的顺序组成的字符串
- 两个值指向同一个对象
- 两个值都是数字并且都是正零 +0
- 两个值都是数字都是负零 -0
- 两个值都是数字都是 NaN
- 两个值都是数字都是除零和 NaN 外的其它同一个数字
由此可看出,Object.is() 可以对基本数据类型 null、undefined、number、string、boolean做出非常精确的比较,但是对于引用数据类型是不能直接比较的。
我们继续剖析 shallowEqual:
shallowEqual 剖析
// React 版本:16.13.1
// shared/shallowEqual.js
import is from './objectIs';
// 原型链上的方法 检查一个属性是否是对象的自身属性
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* 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 {
// 首先对基本类型作比较
// 若是同引用会返回 true
if (is(objA, objB)) {
return true;
}
// 由于 Object.is() 对基本数据类型做的是精确的比较,所以如果不等
// 只有一种情况是误判的,那就是 Object
// 所以在判断两个对象都不是 object 之后,就可以返回false
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
// 过滤掉基本数据类型之后,就是对 Object对象的比较了
// 取出对象的 key,对 key 的长度进行比较
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// key 的长度不相等,返回 false,也就是比较的两个对象不相等
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
// key 相等的情况下,循环比较 value 值
for (let i = 0; i < keysA.length; i++) {
// 借用原型链上的方法 hasOwnProperty 来判断 ObjB 里面是否有 ObjA 的key的key值
// 或者对对象的 value 进行一个基本数据类型的比较
// 根据比较的结果返回 false
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
export default shallowEqual;
我们对上面的分析做一个简单的整理:
- 首先通过 Object.is() 方法比较 ObjA 和 ObjB 是否相等,如果相等,返回
true - 如果 ObjA,ObjB 中有任何一个为
null或者数据类型不是object,直接返回false。
Object.is 方法用于对基本数据类型做一个精确的比较,如果发生误判,仅可能是 object 类型。但如果其中一个不是 object,就不可能相等了。 - 如果 ObjA 和 ObjB 包含的顶层 key 的 length 不同,直接返回
false。 - 将 ObjA 和 ObjB 中的 key/value 一一对比,如果 ObjB 存在 ObjA 没有的键或者相同键对应的值不同,就返回
false。 - 使用反证法,通过所有不相等判断之后,只能是相等的,返回
true。
由上面的分析可以看出,当对比的类型为 Object 并且 key 的长度相等的时候,浅比较也仅仅是用 Object.is() 对Object ( props 或 state ) 的 value 做了一个基本数据类型的比较,也就是只比较了第一层的value,对于深层嵌套的数据是无法比较的。
因此,如果要触发组件重新渲染,不能直接修改之前的引用值,而是重新声明一个变量(Array.prototype.concat(), Object.assign()),才能触发组件重新渲染。