相等性比較在日常開發(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ù)字并且
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 方法保持一致。
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
深度比較常用的庫:
-
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
shallowEqual與deepEqual都可以對基本數(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ù)類型的比較,對于性能的影響不大。 -
shallowEqual和deepEqual主要用于復(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)生新的 onClick 和 items 屬性傳遞給 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 都是相同的 onClick 和 items 屬性值,組件緩存就會起到作用。
我們也可以通過 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 依此比較 a 和 b 這兩個依賴項是否與之前渲染時的值相等,如果不相等,則會重新調(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;
});
});