相等比較總結(jié)

相等性比較在日常開發(fā)中比較常見,這里我們主要介紹以下四種方式:

  • === 嚴(yán)格相等
  • Object.is
  • shallowEqual 淺比較
  • deepEqual 深度比較

比較方式

===

嚴(yán)格相等。嚴(yán)格相等在進(jìn)行比較時,不進(jìn)行隱式的類型轉(zhuǎn)換。如果下列任何一項成立,則兩個值相同:

  • 兩個值都是undefined
  • 兩個值都是null
  • 兩個值都是true或兩個值都是false
  • 兩個值是由相同個數(shù)的字符按照相同的順序組成的字符串
  • 兩個值指向同一個對象
  • 兩個值都是同一個數(shù)字
  • +0 和 -0
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()

判斷兩個值是否是相同的值。如果下列任何一項成立,則兩個值相同:

  • 兩個值都是 undefined
  • 兩個值都是 null
  • 兩個值都是 true 或者都是 false
  • 兩個值是由相同個數(shù)的字符按照相同的順序組成的字符串
  • 兩個值指向同一個對象
  • 兩個值都是數(shù)字并且
    • 都是正零 +0
    • 都是負(fù)零 -0
    • 都是 NaN
    • 都是除零和 NaN 外的其它同一個數(shù)字
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

淺比較,主要用于對引用數(shù)據(jù)類型的比較。具體實現(xiàn):

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

// 這個函數(shù)實際上是Object.is()的實現(xiàn)
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) {
  // 首先對基本數(shù)據(jù)類型的比較
  if (is(objA, objB)) return true;
  /**
   * 由于Obejct.is()可以對基本數(shù)據(jù)類型做一個精確的比較, 所以如果不等
   * 只有一種情況是誤判的,那就是object,所以在判斷兩個對象都不是object
   * 之后,就可以返回false了
   */
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // 過濾掉基本數(shù)據(jù)類型之后,就是對對象的比較了
  // 首先拿出key值,對key的長度進(jìn)行對比
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  // 長度不等直接返回false
  if (keysA.length !== keysB.length) return false;
  // key相等的情況下,再去循環(huán)比較key值對應(yīng)的value
  for (let i = 0; i < keysA.length; i++) {
    // key值相等的時候
    // 借用原型鏈上真正的 hasOwnProperty 方法,判斷ObjB里面是否有A的key的key值
    // 最后,對對象的value進(jìn)行一個基本數(shù)據(jù)類型的比較,返回結(jié)果
    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

從上面的示例可以看出,當(dāng)對比的類型為 Object 的時候并且 key 的長度相等的時候,淺比較也僅僅是用 Object.is()對 Object 的 value 做了一個基本數(shù)據(jù)類型的比較,所以如果 key 里面是對象的話,有可能出現(xiàn)比較不符合預(yù)期的情況,所以淺比較是不適用于嵌套類型的比較的。

常用的淺比較的庫:

  • react-redux | shallowEqual

    上述代碼實現(xiàn)與 react-redux 中的 shallowEqual 方法保持一致。

  • shallowEqual

deepEqual

深度比較。不僅能對一般數(shù)據(jù)進(jìn)行比較還能比較數(shù)組和 json 等數(shù)據(jù),可以進(jìn)行更深層次的數(shù)據(jù)比較。

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

深度比較常用的庫:

  • react-fast-compare

    import isEqual from 'react-fast-compare';
    
    console.log(isEqual({ foo: 'bar' }, { foo: 'bar' })); //true
    

不同比較方式之間區(qū)別

以上四種方式都是判斷相等,但是===Object.is只能判斷基本數(shù)據(jù)類型之間的相等,不能判斷引用類型之間的相等。shallowEqual可以判斷引用類型之間的相等,但是并不能準(zhǔn)確判斷嵌套類型的數(shù)據(jù)。而deepEqual不僅能進(jìn)行基本數(shù)據(jù)類型之間的比較,還能進(jìn)行更深層次的比較。

=== 和 Object.is()

=== 和 Object.is() 都是用來判斷兩個值是否相等,并且判斷邏輯基本保持一致。主要區(qū)別在于以下兩點:

  • +0 、-0

    ===將數(shù)值+0-0視為相等,而 Object.is(
    +0,-0)則會返回 false。

  • NaN

    ===將 NaN 與 NaN 視為不等,而 Object.is(NaN,NaN)則返回 true。

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

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

從上述示例來看,顯然Object.is()的判斷結(jié)果更符合我們的預(yù)期,這是因為它的實現(xiàn)對+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()在比較對象類型的數(shù)據(jù)時,只要不是同一個對象,均會判定為 false,而shallowEqual會比較兩個對象的key及其對應(yīng)的值,如果都相等,則會判定為 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都可以對基本數(shù)據(jù)類型進(jìn)行比較還可以對引用數(shù)據(jù)類型進(jìn)行比較,但是shallowEqual只能滿足一層比較,不能進(jìn)行嵌套數(shù)據(jù)的比較,而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

通過上述對比可以發(fā)現(xiàn):

  • ===Object.is主要用于基本數(shù)據(jù)類型的比較,對于性能的影響不大。
  • shallowEqualdeepEqual主要用于復(fù)雜數(shù)據(jù)結(jié)構(gòu)的相等比較,性能損耗較大,尤其是 deepEqual。

主要用途

目前我們在項目中用到相等性比較,一般都與緩存計算以性能優(yōu)化相關(guān)。比如React.memo、React.useMemo等的實現(xiàn)其實都是依賴相等性比較。下面我們來分析一下項目中常用的幾個方法所用的比較方式。

React.memo

默認(rèn)情況下 React.memo 對屬性對象進(jìn)行淺比較。舉例說明如何發(fā)揮出 React.memo 的緩存特性,減少重復(fù)渲染。

被緩存的組件:

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

破壞緩存的使用方式:

import MyComponent from './MyComponent';

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

每當(dāng) Demo 組件重繪時,都會產(chǎn)生新的 onClickitems 屬性傳遞給 MyComponent 組件,讓其相等性比較為 false,導(dǎo)致緩存失效。

補救措施如下:

import MyComponent from './MyComponent';

const items = [1, 2, 3];

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

這樣,每當(dāng) Demo 組件重繪時,傳遞給 MyComponent 都是相同的 onClickitems 屬性值,組件緩存就會起到作用。

我們也可以通過 React.memo() 的第二個參數(shù)指定比較函數(shù),以自定義屬性對象的相等性比較,如下所示:

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

默認(rèn)使用 Object.is 對依賴項進(jìn)行相等性比較。

我們來看看怎么用 React.useMemo

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

把“創(chuàng)建”函數(shù)和依賴項數(shù)組作為參數(shù)傳入useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優(yōu)化有助于避免在每次渲染時都進(jìn)行高開銷的計算。

React 會在組件重繪時,用 Object.is 依此比較 ab 這兩個依賴項是否與之前渲染時的值相等,如果不相等,則會重新調(diào)用“創(chuàng)建”函數(shù),生成新的值。

React.useCallback

同 React.useMemo 一樣都是默認(rèn) Object.is 形式的相等比較。

react-redux 中的 useSelector

默認(rèn)使用 Object.is() 比較方式,但是支持傳入第二個參數(shù)進(jìn)行 shallowEqual 淺比較。

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

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

當(dāng)然,特殊情況下你也可以傳入深度比較函數(shù),讓 useSelector 使用深度比較,只是這樣可能會成為性能瓶頸:

import deepEqual from 'react-fast-compare';

// ?? 不到萬不得已,別用深度比較。
const selectedData = useSelector(selectorReturningDeepObject, deepEqual);

lodash/memoize

使用Object.is()進(jìn)行相等比較,且緩存一直存在。

不可變數(shù)據(jù)

通過上述描述,我們可以看出目前常用方法中用到的比較方式基本都是 Object.is ,其中 React.memo、useSelector 支持自定義比較邏輯。

由于淺比較和深度比較非常耗費性能,尤其是深度比較,所以在日常開發(fā)中我們應(yīng)盡量避免使用這兩種比較方式。為了更好地避免淺比較和深度比較,我們可以在日常開發(fā)中使用不可變數(shù)據(jù)的小技巧處理數(shù)據(jù),這里我們主要依賴immer來做數(shù)據(jù)不可變。

在程序中組合使用不可變數(shù)據(jù)和React.memo,是解決性能問題的重要手段。使用 immer 可以簡化不可變數(shù)據(jù)的編程。以待辦列表為例看下二者是如何使用的:

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: '學(xué)些相等性比較' },
  { id: '2', title: '學(xué)習(xí) React' },
  { id: '3', title: '學(xué)些 immer' },
];
function TodoList() {
  const [todos, setTodos] = useState(defaultTodos);

  // 別忘了給回調(diào)函數(shù)添加上 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>
  );
}

當(dāng)改變一條待辦事項的標(biāo)題時,由于只是變更了此條待辦事項的狀態(tài)對象,其他待辦事項的狀態(tài)對象并沒有發(fā)生改變,所以在 React.memo 作用下,只會引起此條待辦事項的重繪。

示例代碼中變更待辦事項標(biāo)題的部分:

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;
  });
});
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容