React PureComponent 中的浅比较

640 阅读4分钟

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

我们对上面的分析做一个简单的整理:

  1. 首先通过 Object.is() 方法比较 ObjA 和 ObjB 是否相等,如果相等,返回true
  2. 如果 ObjA,ObjB 中有任何一个为null或者数据类型不是object,直接返回false
    Object.is 方法用于对基本数据类型做一个精确的比较,如果发生误判,仅可能是 object 类型。但如果其中一个不是 object,就不可能相等了。
  3. 如果 ObjA 和 ObjB 包含的顶层 key 的 length 不同,直接返回false
  4. 将 ObjA 和 ObjB 中的 key/value 一一对比,如果 ObjB 存在 ObjA 没有的键或者相同键对应的值不同,就返回false
  5. 使用反证法,通过所有不相等判断之后,只能是相等的,返回true

由上面的分析可以看出,当对比的类型为 Object 并且 key 的长度相等的时候,浅比较也仅仅是用 Object.is() 对Object ( props 或 state ) 的 value 做了一个基本数据类型的比较,也就是只比较了第一层的value,对于深层嵌套的数据是无法比较的。

因此,如果要触发组件重新渲染,不能直接修改之前的引用值,而是重新声明一个变量(Array.prototype.concat(), Object.assign()),才能触发组件重新渲染。