vlambda博客
学习文章列表

React 中的浅比较是如何工作的?

React 中浅比较的概念无处不在,它在不同的流程中起着关键的作用。比如,React Hooks 中的依赖数组,通过 React.memo 进行记忆。在 React 的官方文档中也多次提到浅比较的概念,下面就来看看 React 中的浅比较是如何工作的!

要想理解浅比较的概念,最直接的方法就是研究 React 相关的源码,下面就来看看React中的shallowEqual.js 文件:

import is from './objectIs';
import hasOwnProperty from './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++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

这里执行了多步操作,下面就来将其拆分并逐步解释这些功能。先来看看函数的定义,这个函数接受两个需要比较的对象,这里的代码使用 Flow 作为类型检查系统。两个函数参数都是使用特殊的混合 Flow 类型定义,类似于 TypeScript 的 unknown,它表明函数可以是任何类型的值。

function shallowEqual(objA: mixed, objB: mixed): boolean {
 // ...
}

之后使用 React 内部对象的 is 函数将两个函数参数进行比较。导入的 is 函数只不过是JavaScript 的 Object.is 函数的polyfill 版本。这个比较函数基本上等同于常见的 === 运算符,但有两个例外:

  • Object.is 认为 +0-0 不相等,而 === 认为它们相等;
  • Object.is 认为 Number.NaNNaN 相等,而 === 认为它们不相等。

基本上,第一个条件语句可以处理所有简单的情况:如果两个函数参数具有相同的值,对于原始类型,或引用相同的对象(数组和对象),那么通过浅比较认为它们相等的。

import is from './objectIs';

function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }
 // ...
}

在处理两个函数参数值相等或者引用同一个对象的所有简单情况之后,来看看更复杂的结构:数组和对象。

为了确保现在要处理的是两个复杂的结构,代码会检查任一参数是不是object类型或者等于null,前者用来确保我们处理的数组或对象,后者用来过滤掉空值,因为typeof null的结果也是 object。如果任何一个条件成立,那两个参数一定是不相等的(否则前面的条件语句就会将它们过滤掉),因此浅比较直接返回false。

function shallowEqual(objA: mixed, objB: mixed): boolean {
 // ...

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

 // ...
}

现在就可以确保我们处理的是数组和对象了,接下来我们深入研究复杂数据结构的值,并在两个函数参数之间进行比较。在此之前,先来检查两个参数中值的数量是否相等,如果不相等,直接就可以确定两个值是不相等的。对于对象,得到的keys数组就是由实际的key组成的;对于数组,得到keys数组数是由字符串类型的数组索引组成的。

function shallowEqual(objA: mixed, objB: mixed): boolean {
 // ...

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

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

 // ...
}

最后一步,按照 key 来迭代两个函数参数的值,并逐个验证他们是否是相等的。为此,代码使用到了上一步中生成的keys数组,使用 hasOwnProperty 检查key是否实际上是参数的属性,并使用 Object.is 函数进行比较。

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;
}

如果任何两个key对应的值是不相等的,那两个对象肯定就是不相等的,因此直接人会false,结束循环。如果所有的值都是相等的,就返回 true

至此,我们通过 React 源码学习了 React 中的浅比较,下面来总结一下:

  • 浅比较使用的是 Object.is 函数,而不是使用严格相等 === 运算符;
  • 通过浅比较,空对象和空数组是等价的;
  • 通过浅比较,以数组索引为 key 和数组值为 value的对象是等价的,比如: { 0: 2, 1: 3 } 等价于 [2, 3]
  • 由于通过 Object.is比较的 +0-0Number.NaNNaN是不相等的,所以在复杂结构中比较时,这也是适用的;
  • 虽然 {} 和 [] 浅比较是相等的,但是嵌套在对象中是不相等的,比如: { someKey: {} }{ someKey: [] } 是不相等的。

源码:https://github.com/facebook/react/blob/main/packages/shared/shallowEqual.js

往期推荐: