相等比较总结

2019-12-19  本文已影响0人  紫诺_qiu

相等性比较在日常开发中比较常见,这里我们主要介绍以下四种方式:

比较方式

===

严格相等。严格相等在进行比较时,不进行隐式的类型转换。如果下列任何一项成立,则两个值相同:

console.log(undefined === undefined); // true
console.log(undefined === null); // false
console.log(null === null); // true

// 特例
console.log(+0 === -0); // true
console.log(NaN === NaN); // false

Object.is()

判断两个值是否是相同的值。如果下列任何一项成立,则两个值相同:

Object.is('foo', 'foo'); // true
Object.is(window, window); // true

Object.is('foo', 'bar'); // false
Object.is([], []); // false

// 特例
Object.is(0, -0); // false
Object.is(0, +0); // true
Object.is(-0, -0); // true
Object.is(NaN, 0 / 0); // true

shallowEqual

浅比较,主要用于对引用数据类型的比较。具体实现:

// 用原型链的方法
const hasOwn = Object.prototype.hasOwnProperty;

// 这个函数实际上是Object.is()的实现
function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y;
  } else {
    return x !== x && y !== y;
  }
}

export default function shallowEqual(objA, objB) {
  // 首先对基本数据类型的比较
  if (is(objA, objB)) return true;
  /**
   * 由于Obejct.is()可以对基本数据类型做一个精确的比较, 所以如果不等
   * 只有一种情况是误判的,那就是object,所以在判断两个对象都不是object
   * 之后,就可以返回false了
   */
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // 过滤掉基本数据类型之后,就是对对象的比较了
  // 首先拿出key值,对key的长度进行对比
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  // 长度不等直接返回false
  if (keysA.length !== keysB.length) return false;
  // key相等的情况下,再去循环比较key值对应的value
  for (let i = 0; i < keysA.length; i++) {
    // key值相等的时候
    // 借用原型链上真正的 hasOwnProperty 方法,判断ObjB里面是否有A的key的key值
    // 最后,对对象的value进行一个基本数据类型的比较,返回结果
    if (!hasOwn.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
      return false;
    }
  }

  return true;
}

简单示例

const objA = { a: 1, b: 2 };
const objB = { a: 1, b: 2 };

Object.is(objA, objB); // false
shallowEqual(objA, objB); //true

从上面的示例可以看出,当对比的类型为 Object 的时候并且 key 的长度相等的时候,浅比较也仅仅是用 Object.is()对 Object 的 value 做了一个基本数据类型的比较,所以如果 key 里面是对象的话,有可能出现比较不符合预期的情况,所以浅比较是不适用于嵌套类型的比较的。

常用的浅比较的库:

deepEqual

深度比较。不仅能对一般数据进行比较还能比较数组和 json 等数据,可以进行更深层次的数据比较。

const objA = { a: 1, b: { c: 1 } };
const objB = { a: 1, b: { c: 1 } };

deepEqual(objA, objB); // true
deepEqual([1, 2], [1, 2]); // true
deepEqual([[1, 2], [2]], [[1, 2], [2]]); // true

深度比较常用的库:

不同比较方式之间区别

以上四种方式都是判断相等,但是===Object.is只能判断基本数据类型之间的相等,不能判断引用类型之间的相等。shallowEqual可以判断引用类型之间的相等,但是并不能准确判断嵌套类型的数据。而deepEqual不仅能进行基本数据类型之间的比较,还能进行更深层次的比较。

=== 和 Object.is()

=== 和 Object.is() 都是用来判断两个值是否相等,并且判断逻辑基本保持一致。主要区别在于以下两点:

+0 === -0; // true
Object.is(+0, -0); // false

NaN === NaN; // false
Object.is(NaN, NaN); //true

从上述示例来看,显然Object.is()的判断结果更符合我们的预期,这是因为它的实现对+0,-0,NaN的情况做了特殊处理。

function(x, y) {
    // SameValue algorithm
    if (x === y) {
     // 处理为+0 != -0的情况
      return x !== 0 || 1 / x === 1 / y;
    } else {
    // 处理 NaN === NaN的情况
      return x !== x && y !== y;
    }
};

===、Object.is() 和 shallowEqual

===Object.is()在比较对象类型的数据时,只要不是同一个对象,均会判定为 false,而shallowEqual会比较两个对象的key及其对应的值,如果都相等,则会判定为 true。

const arrA = [1, 2];
const arrB = [1, 2];
const objA = { a: 1, b: 2 };
const objB = { a: 1, b: 2 };

arrA === arrB; // false
Object.is(arrA, arrB); //false
shallowEqual(arrA, arrB); // true

objA === objB; //false
Object.is(objA, objB); // false
shallowEqual(objA, objB); // true

shallowEqual 与 deepEqual

shallowEqualdeepEqual都可以对基本数据类型进行比较还可以对引用数据类型进行比较,但是shallowEqual只能满足一层比较,不能进行嵌套数据的比较,而deepEqual支持更深层次的比较。

const objA = { a: 1, b: 2 };
const objB = { a: 1, b: 2 };
const objC = { a: 1, b: { c: 3 } };
const objD = { a: 1, b: { c: 3 } };

shallowEqual(objA, objB); // true
deepEqual(objA, objB); // true

shallowEqual(objC, objD); // false
deepEqual(objC, objD); // true

通过上述对比可以发现:

主要用途

目前我们在项目中用到相等性比较,一般都与缓存计算以性能优化相关。比如React.memoReact.useMemo等的实现其实都是依赖相等性比较。下面我们来分析一下项目中常用的几个方法所用的比较方式。

React.memo

默认情况下 React.memo 对属性对象进行浅比较。举例说明如何发挥出 React.memo 的缓存特性,减少重复渲染。

被缓存的组件:

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

破坏缓存的使用方式:

import MyComponent from './MyComponent';

function Demo() {
  // 🔴 缓存失效
  return <MyComponent onClick={() => console.log('click')} items={[1, 2, 3]} />;
}

每当 Demo 组件重绘时,都会产生新的 onClickitems 属性传递给 MyComponent 组件,让其相等性比较为 false,导致缓存失效。

补救措施如下:

import MyComponent from './MyComponent';

const items = [1, 2, 3];

function Demo() {
  const handleClick = useCallback(() => {
    console.log('click');
  }, []);
  // ✅ 缓存有效
  return <MyComponent onClick={handleClick} items={items} />;
}

这样,每当 Demo 组件重绘时,传递给 MyComponent 都是相同的 onClickitems 属性值,组件缓存就会起到作用。

我们也可以通过 React.memo() 的第二个参数指定比较函数,以自定义属性对象的相等性比较,如下所示:

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  const { items: prevItems, ...prevRest } = prevProps;
  const { items: nextItems, ...nextRest } = nextProps;

  // 😔 不到万不得已,别用深度比较。这段代码只是示例。
  return shallowEqual(prevRest, nextRest) && deepEqual(prevItems, nextItems);
}

export default React.memo(MyComponent, areEqual);

下面的使用方式缓存是有效的:

import MyComponent from './MyComponent';

function Demo() {
  const handleClick = useCallback(() => {
    console.log('click');
  }, []);

  return <MyComponent onClick={handleClick} items={[1, 2, 3]} />;
}

React.useMemo

默认使用 Object.is 对依赖项进行相等性比较。

我们来看看怎么用 React.useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

React 会在组件重绘时,用 Object.is 依此比较 ab 这两个依赖项是否与之前渲染时的值相等,如果不相等,则会重新调用“创建”函数,生成新的值。

React.useCallback

同 React.useMemo 一样都是默认 Object.is 形式的相等比较。

react-redux 中的 useSelector

默认使用 Object.is() 比较方式,但是支持传入第二个参数进行 shallowEqual 浅比较。

import { shallowEqual, useSelector } from 'react-redux';

// later
const selectedData = useSelector(selectorReturningObject, shallowEqual);

当然,特殊情况下你也可以传入深度比较函数,让 useSelector 使用深度比较,只是这样可能会成为性能瓶颈:

import deepEqual from 'react-fast-compare';

// 😔 不到万不得已,别用深度比较。
const selectedData = useSelector(selectorReturningDeepObject, deepEqual);

lodash/memoize

使用Object.is()进行相等比较,且缓存一直存在。

不可变数据

通过上述描述,我们可以看出目前常用方法中用到的比较方式基本都是 Object.is ,其中 React.memouseSelector 支持自定义比较逻辑。

由于浅比较和深度比较非常耗费性能,尤其是深度比较,所以在日常开发中我们应尽量避免使用这两种比较方式。为了更好地避免浅比较和深度比较,我们可以在日常开发中使用不可变数据的小技巧处理数据,这里我们主要依赖immer来做数据不可变。

在程序中组合使用不可变数据和React.memo,是解决性能问题的重要手段。使用 immer 可以简化不可变数据的编程。以待办列表为例看下二者是如何使用的:

TodoItem.tsx

import React, { useCallback } from 'react';

function TodoItem({ item, onTitleChange }) {
  const handleChange = useCallback(
    (event) => {
      onTitleChange(item.id, event.target.value);
    },
    [item.id, onTitleChange],
  );

  return <input value={item.title} onChange={handleChange} />;
}

export default React.memo(TodoItem);

TodoList.tsx

import React, { useCallback, useState } from 'react';
import produce from 'immer';
import TododItem from './TodoItem';

const defaultTodos = [
  { id: '1', title: '学些相等性比较' },
  { id: '2', title: '学习 React' },
  { id: '3', title: '学些 immer' },
];
function TodoList() {
  const [todos, setTodos] = useState(defaultTodos);

  // 别忘了给回调函数添加上 useCallback
  const handleTitleChange = useCallback((itemId: string, title: string) => {
    setTodos(
      produce((draft) => {
        const todoItem = draft.find((item) => item.id === itemId);
        todoItem.title = title;
      }),
    );
  }, []);

  return (
    <div>
      {todos.map((todo) => (
        <TodoItem key={todo.id} item={todo} onTitleChange={handleTitleChange} />
      ))}
    </div>
  );
}

当改变一条待办事项的标题时,由于只是变更了此条待办事项的状态对象,其他待办事项的状态对象并没有发生改变,所以在 React.memo 作用下,只会引起此条待办事项的重绘。

示例代码中变更待办事项标题的部分:

setTodos(
  produce((draft) => {
    const todoItem = draft.find((item) => item.id === itemId);
    todoItem.title = title;
  }),
);

等价于:

setTodos((state) => {
  return state.map((item) => {
    if (item.id === itemId) {
      return { ...item, title };
    }
    return item;
  });
});
上一篇下一篇

猜你喜欢

热点阅读