最近在接觸到的幾個項目發(fā)現(xiàn),有的項目使用很多地方用到了 React.memo,有的項目一次都沒有使用。那么,項目中到底要不要使用 memo 呢?看完這篇文章,你就會有答案。
1、什么是memo
???? ????memo 允許你的組件在 props 沒有改變的情況下跳過重新渲染。
1.1、用法
const Greeting = React.memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});
export default Greeting;
參數(shù)
???? ????Component:要進行記憶化的組件。memo 不會修改該組件,而是返回一個新的、記憶化的組件。它接受任何有效的 React 組件,包括函數(shù)組件和 forwardRef 組件。
???? ????arePropsEqual(可選):一個函數(shù),接受兩個參數(shù):組件的前一個 props 和新的 props。如果舊的和新的 props 相等,即組件使用新的 props 渲染的輸出和表現(xiàn)與舊的 props 完全相同,則它應(yīng)該返回 true。否則返回 false。通常情況下,你不需要指定此函數(shù)。默認情況下,React 將使用 Object.is 比較每個 prop。
1.2、與 PureComponent 有何差別
???? ????PureComponent 適用于類組件,當(dāng) props 和 state 與之前保持一致時會跳過重新渲染。
???? ????memo 適用于函數(shù)組件,當(dāng) props 和之前一致時,會跳過重新渲染。這里不包含 state,React 官方的說法是在函數(shù)組件中,即使沒有 memo,調(diào)用具有相同 state 的 set 函數(shù),默認已經(jīng)阻止了重新渲染。這句話不太容易理解,翻譯一下就是當(dāng) setState 的值和引用沒有變化時,React 會阻止組件的重新渲染。
2、代碼解析
2.1、源碼
function memo(type, compare) {
// 核心代碼
return {
$$typeof: REACT_MEMO_TYPE,
type: type,
compare: compare === undefined ? null : compare
};
}
???? ????memo 的源碼比較簡單,在返回一個記憶化組件的同時,給組件添加 $$typeof 標記。在創(chuàng)建 Fiber 時,會根據(jù) $$typeof 的值,給 fiber.tag 賦值為 MemoComponent(MemoComponent=14)。
2.2、工作流
???? ????當(dāng) state(可以是任何組件的 state)改變時,React 會從根節(jié)點開始遍歷 Fiber 樹,在這個過程中會對每一個 Fiber 節(jié)點調(diào)用 beginWork 方法。當(dāng) tag 為 MemoComponent 時,會調(diào)用 updateMemoComponent 方法。當(dāng)我們在使用 memo 沒有傳入第二個參數(shù)時,會調(diào)用默認的 shallowEqual 對 prevProps 和 nextProps 進行淺比較,當(dāng)結(jié)果為 true 時,組件就不會重新渲染。
function beginWork(...) {
// ...
switch (workInProgress.tag) {
case MemoComponent: {
return updateMemoComponent(...);
}
}
}
function updateMemoComponent(...) {
// ...
if (updateExpirationTime < renderExpirationTime) {
// Default to shallow comparison
var compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current$$1.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(...);
}
}
var newChild = createWorkInProgress(...);
return newChild;
}
3、注意事項
3.1、當(dāng)組件屬性包含方法和對象時,要結(jié)合useMemo和useCallback一起使用。
???? ????因為函數(shù)組件的特性,當(dāng)組件state或props發(fā)生變化時,其內(nèi)部的變量會重新聲明,如果是引用類型的變量,其引用地址也會發(fā)生改變。所以,像下面的用法,達不到預(yù)期的效果。
錯誤用法:
import { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
const userInfo = { name,age:18 };
const onClick = () => {};
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)}/>
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)}/>
</label>
<Greeting userInfo={userInfo} onClick={onClick} />
</>
);
}
const Greeting = memo(function Greeting({ userInfo, onClick }) {
return <h3>Hello{userInfo.name && ', '}{userInfo.name}!</h3>;
});
正確用法:
const userInfo = useMemo(() => {
return { name, age: 18 };
}, [name]);
const onClick = useCallback(() => {
// do something
},[]);
// 使用結(jié)合useMemo、useCallback使用
<Greeting userInfo={userInfo} onClick={onClick} />
// 展開對象 僅適用于對象屬性都是基本類型的情況
<Greeting {...obj} />
3.2、路由入口組件不要使用memo
因為memo返回的記憶化組件,通常是作為子組件來使用。當(dāng)頁面加載完成,路由入口組件的props,通常是不會再次變化的,所以使用memo包裹,起不到性能優(yōu)化的效果。
// 路由配置
const routes = [
{
path: '/orderList',
component: '@/pages/orderList',
title: '訂單列表'
}
]
// 錯誤示例
const OrderList = () => {
return (
<div>....</div>
)
}
export default memo(OrderList)
4、結(jié)論
???? ????說了這么多,那項目中到底要不要使用 memo 呢?React 官方的回答是,當(dāng)你的代碼沒有 memo 就無法運行時,再去使用。為什么官方不建議在任何地方都使用 memo 呢,我總結(jié)了以下三點原因(個人觀點):
1、大部分場景下,memo 對性能提升的效果不是很明顯。
2、使代碼變得冗余、不易讀。
3、占用更多的內(nèi)存(memo 不會修改原組件,而是返回一個新的、記憶化的組件,需要結(jié)合 useMemo、useCallback 等 Hook 一起使用)。
???? ????當(dāng)我們遇到性能問題時,首先要做的是檢查代碼寫得是否有問題,能不能使用其他手段來優(yōu)化代碼,例如,減少不必要的狀態(tài)提升,而不是直接無腦使用 memo。通常情況下,當(dāng)你的項目有很多細粒度(繪圖、數(shù)據(jù)可視化、拖拽等)的交互時,使用 memo 對性能的提示效果才會更明顯。