本文翻译自https://www.chakshunyu.com/blog/how-does-shallow-comparison-work-in-react/
浅比较这个概念在React开发过程中很常见。它在不同的过程中扮演着关键的角色,也可以在React组件生命周期的几个地方找到。判断class组件是否应该更新、React hood的依赖数组、通React.memo
缓存处理等例子
如果曾经阅读过官方的React文档,我们可能会经常到看到浅比较这个概念。但通常只是一个比较简单的解释。所以,本文将研究浅比较的概念,它到底是什么、如何工作,并会得到一些我们可能不知道的结论
深入浅比较的实现
最直接了解浅比较的方式就是去深入它的实现。相应的代码可以在React Github项目的shared
包中的shallowEqual.js
找到。代码如下
import is from './objectIs';
import hasOwnProperty from './hasOwnProperty';
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;
}
这个函数做了不少事情,我们一步一步看这个函数
代码语言:javascript复制function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
}
函数接收两个入参作为被比较的对象。这个代码使用了Flow
作为类型检测系统而不是使用TypeScript
。两个函数的参数都使用了Flow
中的mixed
类型(类似TypeScript
中的unknnown
)。这表明它们可以是任意类型。
import is from './objectIs';
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
// ...
}
首先使用React的内部实现的is
方法对两个函数参数进行比较。这个引入的is
内部方法和js中的Object.js
几乎没有区别。这个比较函数和常用的===
基本相同,除了两个例外
Object.is
将0
和-0
当作不相等,而===
把他们当作相等Object.is
把Number.NaN
和Number.NaN
当作相等,而===
把他们当作不相等
基本上第一个条件分支能处理如下简单的情况:如果两个参数有相同的值,如原始值相等、或对象的引用相等,它们会被认为相等
代码语言:javascript复制function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
// ...
}
处理了简单情况下的值相等或者对象引用相等后我们需要去比较更复杂的结构。如果其中一个参数是原始值,前面的比较仍然会漏掉这种情况
为了确保我们下面是比较两个复杂的数据结构,我们还需要检查是否其中一个参数不是对象或者是null。前一个检查确保我们处理的两个参数是对象或数组,而后一个检查是过滤掉null
,因为的typeof null === 'object'
。如果两个条件都成立那么处理的两个参数肯定是不相等的(否则前面的判断就会将它们过滤),所以浅比较返回false。
function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// ...
}
现在可以确定我们只处理数组和对象。因此可以把重点放在复杂数据结构的比较上
首先,我们可以简单比较它们的键的数量是否相等。如果不是,他们就不会浅比较相等,这可以提高检查的效率。我们使用Object.keys
获取它们的键的数量。对于对象,键数组由实际的键组成;而对于数组,键数组将由数组的索引组成。
import hasOwnProperty from './hasOwnProperty';
function shallowEqual(objA: mixed, objB: mixed): boolean {
// ...
// 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;
}
最后,我们遍历两个函数参数的值并逐个比较它们是否相等。使用上一步中生成的键数组,并使用hasOwnProperty
检查键是否实际上是对象自身的属性,使用Object.is
函数进行值比较
如果存在对象上的某个值不相等,那么通过浅比较就可以认为它们不相等。因此可以提前结束循环,并直接shallow wEqual
函数返回false。如果所有的值都是相等那么我们可以通过浅比较函数判断两个参数相等,函数返回true
有趣的东西
我们已经了解了简单的比较和它背后的实现,也可以从中知道到一些有趣的东西:
- 浅比较并不是使用全等
===
,而是使用Object.is
- 浅比较中,空对象和空数组会被认为相等
- 浅比较中,一个以索引值作为键的对象和一个在相应各下标处具有相同值的数组相等。如
{0:2,1:3}
等于[2,3]
- 由于使用
Object.is
而不是使用===
。0
和-0
在浅比较中是不相等的。并且NaN
和NaN
也认为不相等。这也适用于复杂结构内部的比较 - 虽然两个直接创建的对象(或数组)通过浅比较是相等的(
{}
和[]
),但嵌套的数组、对象是不相等的。如{someKey:{}
和{someKey:[]}
浅比较是不相等的)