在對 React 項目做性能優(yōu)化的時候欠母,memeo爬早、useMemo氯夷、useCallback 三個API總是形影不離捍掺。
一撼短、memo
1.memo作用
在 React 的渲染流程中,一般來說挺勿,父組件的某個狀態(tài)發(fā)生改變阔加,那么父組件會重新渲染,父組件所使用的所有子組件满钟,都會強制渲染胜榔。而在某些場景中,子組件并沒有使用父組件傳入的沒有發(fā)生更改的狀態(tài)時湃番,子組件重新渲染是沒有必要的夭织。因此有了 React.memo
2.memo 的使用
memo 是個高階組件, 結(jié)合了 PurComponent 和 shouldComponentUpdate 功能吠撮,會對傳入的 props 進行淺比較尊惰,來決定是否更新被包裹的組件
memo 接受兩個參數(shù):
- WrapComponent:你要優(yōu)化的組件
- (prev, next) => boolean:通過對比 prev(舊 props),next(新 props)是否一致泥兰,返回 true(不更新)弄屡、false(更新)
注意:memo 只針對 props 來決定是否渲染,且是淺比較
現(xiàn)在我們來看一個的例子:
import { useState } from 'react';
const Child = () => (
<div>{console.log('子組件渲染了')}</div>
);
function Parent() {
const [status, setStatus] = useState(true);
return (
<div>
<Child />
<button
onClick={() =>
setStatus(!status)
}>
{status ? 'on' : 'off'}
</button>
</div>
);
}
export default Parent;
運行結(jié)果如下:
在上面的例子中鞋诗,父組件中的狀態(tài) status和 Child 組件沒有關系膀捷,當我點擊按鈕時,status 發(fā)生改變削彬,此時父組件重新渲染全庸,按鈕文案變?yōu)閛ff,控制臺卻打印出 "子組件又渲染" 的信息融痛,說明子組件也跟著重新渲染了壶笼。而這肯定是不合理的,我們不希望子組件做無關的刷新雁刷,此時我們可以給子組件加上memo
import { useState, memo } from 'react';
const Child = memo(() => (
<div>{console.log('子組件渲染了')}</div>
));
function Parent() {
const [status, setStatus] = useState(true);
return (
<div>
<Child />
<button
onClick={() =>
setStatus(!status)
}>
{status ? 'on' : 'off'}
</button>
</div>
);
}
export default Parent;
此時我們點擊按鈕覆劈,子組件不會被重新渲染
import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo((props) => (
<div>
{props.number}
{console.log('子組件渲染了')}
</div>
));
Child.propTypes = {
number: PropTypes.number,
};
function Parent() {
const [number, setNumber] = useState(1);
return (
<div>
<Child number={number} />
<button onClick={() => setNumber(2)}>
點擊
</button>
</div>
);
}
export default Parent;
在這個例子中,當我們點擊按鈕沛励,傳入子組件的number從1變?yōu)榱?责语,子組件的props發(fā)生了改變,重新渲染
總而言之侯勉,如果組件被 memo 包裹鹦筹,那么組件的 props 不發(fā)生改變時,組件不會重新渲染址貌。這樣铐拐,我們合理的使用 memo 就可以為我們的項目帶來很大的性能優(yōu)化
3.memo 的注意事項
memo 對于新舊 props 的比較默認是淺比較,當我們子組件接收的是一個引用類型的 props 的時候练对,可以自定義比較來決定是否需要使用緩存還是重新渲染
看下面的例子
import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo((props) => (
<div>
{`我叫${props.obj.name}`}
{console.log('子組件渲染了')}
</div>
));
Child.propTypes = {
obj: PropTypes.shape({
name: PropTypes.string,
age: PropTypes.number,
}),
};
function Parent() {
const [obj, setObj] = useState({
name: 'xxx',
age: 18,
});
return (
<div>
<Child obj={obj} />
<button
onClick={() => {
setObj({
name: 'xxx',
age: 19,
});
}}>
點擊
</button>
</div>
);
}
export default Parent;
我們點擊按鈕修改了age遍蟋,子組件的props發(fā)生了變化,重新渲染螟凭。但是子組件中并沒有用到age虚青,我們不需要它重新渲染,這個時候我們可以使用memo的第二個參數(shù)來自定義校驗規(guī)則
import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo(
(props) => (
<div>
{`我叫${props.obj.name}`}
{console.log('子組件渲染了')}
</div>
),
// 新舊name相同就不重新渲染
(prev, next) => {
return prev.obj.name === next.obj.name;
},
);
Child.propTypes = {
obj: PropTypes.shape({
name: PropTypes.string,
age: PropTypes.number,
}),
};
function Parent() {
const [obj, setObj] = useState({
name: 'xxx',
age: 18,
});
return (
<div>
<Child obj={obj} />
<button
onClick={() => {
setObj({
name: 'xxx',
age: 19,
});
}}>
點擊
</button>
</div>
);
}
export default Parent;
這個時候我們點擊按鈕修改age螺男,子組件就不會重新渲染了棒厘。注意:默認情況下(沒有自定義校驗)即使引用對象的屬性值沒發(fā)生變化纵穿,但是地址改變了,也會引起子組件重新渲染奢人,例如上述例子中使用setObj({...obj})
因為緩存本身也是需要開銷的谓媒。如果每一個組件都用 memo 去包裹一下,那么對瀏覽器的開銷就會很大何乎,本末倒置了句惯。
所以我們應該選擇性的用 memo 包裹組件,而不是濫用
二支救、useMemo
1.useMemo 的作用
useMemo 它可以緩存一個結(jié)果抢野,當這個緩存結(jié)果不變時,可以借此來進行性能優(yōu)化各墨。
看下面的例子
import { useState } from 'react';
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = () => {
console.log('計算result');
for (let i = 0; i < 10000; i++) {
i.toString();
}
return 1000;
};
return (
<div>
<div>result: {result()}</div>
<div>number: {number}</div>
<button onClick={() => addNumber()}>
click
</button>
</div>
);
};
export default Parent;
當我們點擊按鈕指孤,number每次點擊都會加1,result方法也會隨著重新計算一遍欲主,每次都要進行大量的for循環(huán)邓厕,很耗費性能,這種情況下我們可以使用useMemo來進行優(yōu)化
2.useMemo 的使用
useMemo 接受兩個參數(shù):
- callback:計算結(jié)果的執(zhí)行函數(shù)
- deps:相關依賴項數(shù)組
最終 useMemo 在執(zhí)行了 callback 后扁瓢,返回一個結(jié)果详恼,這個結(jié)果就會被緩存起來。當 deps 依賴發(fā)生改變的時候引几,會重新執(zhí)行 callback 計算并返回最新的結(jié)果昧互,否則就使用緩存的結(jié)果
我們來把上面的例子用 useMemo 改造一下
import { useState, useMemo } from 'react';
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = useMemo(() => {
console.log('計算result');
for (let i = 0; i < 10000; i++) {
i.toString();
}
return 1000;
}, []);
return (
<div>
<div>result: {result}</div>
<div>number: {number}</div>
<button onClick={() => addNumber()}>
click
</button>
</div>
);
};
export default Parent;
現(xiàn)在不論我們怎么去改變number的值,result都不會重新運行伟桅,這樣就達到了性能優(yōu)化的目的
useMemo 并不是用的越多越好敞掘,緩存本身也需要開銷,一些簡單的計算方法就沒必要使用useMemo
3.useMemo配合memo使用
import { useState, memo } from 'react';
const Child = memo(() => {
console.log('子組件渲染');
return <div>子組件</div>;
});
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = () => {
console.log('計算result');
return 1000;
};
return (
<div>
<div>result: {result}</div>
<div>number: {number}</div>
<button onClick={() => addNumber()}>
click
</button>
<Child result={result} />
</div>
);
};
export default Parent;
上面的例子中楣铁,result函數(shù)作為props傳給了子組件玖雁,即使子組件被memo包裹著,但還是重新渲染了盖腕,這是因為赫冬,父組件重新渲染時,又創(chuàng)建了一個函數(shù)(或者說又開辟了一個內(nèi)存地址)賦值給 result溃列,而 memo 只做淺比較劲厌,發(fā)現(xiàn)地址改變了,所以子組件重新渲染听隐,這個時候就需要使用 useMemo 來進行優(yōu)化
import { useState, memo, useMemo } from 'react';
const Child = memo(() => {
console.log('子組件渲染');
return <div>子組件</div>;
});
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = useMemo(() => {
console.log('計算result');
return 1000;
}, []);
return (
<div>
<div>result: {result}</div>
<div>number: {number}</div>
<button onClick={() => addNumber()}>
click
</button>
<Child result={result} />
</div>
);
};
export default Parent;
此時补鼻,再次點擊按鈕修改 number 后,子組件不會重新更新,達到了性能優(yōu)化的目的
三风范、useCallback
1.useCallback 的作用
useCallback 類似于 useMemo咨跌,只不過 useCallback 用于緩存函數(shù)罷了,同樣可以防止無關的刷新硼婿,對組件做出性能優(yōu)化
2.useCallback 的使用
useCallback 同樣接受兩個參數(shù):
- callback:傳入子組件的函數(shù)
- deps:相關依賴項數(shù)組
最終 useCallback 會把傳入的 callback 緩存起來虑润。當 deps 依賴發(fā)生改變的時候,會重新緩存最新的 callback 加酵,否則就使用緩存的結(jié)果
單獨使用 useCallback 起不到優(yōu)化的作用,反而會增加性能消耗哭当,需要和 memo 一起使用
我們來把上面的例子用 useCallback 改造一下
import {
useState,
memo,
useCallback,
} from 'react';
const Child = memo(() => {
console.log('子組件渲染');
return <div>子組件</div>;
});
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = useCallback(() => {
console.log('計算result');
}, []);
return (
<div>
<div>result: {result}</div>
<div>number: {number}</div>
<button onClick={() => addNumber()}>
click
</button>
<Child result={result} />
</div>
);
};
export default Parent;
點擊按鈕修改 number 后猪腕,子組件不會重新更新,達到了性能優(yōu)化的目的
總結(jié)
memo:
- 父組件重新渲染钦勘,沒有被 memo 包裹的子組件也會重新渲染
- 被 memo 包裹的組件只有在 props 改變后陋葡,才會重新渲染
- memo 只會對新舊 props 做淺比較,所以對于引用類型的數(shù)據(jù)如果發(fā)生了更改彻采,需要返回一個新的地址
- memo 并不是用的越多越好腐缤,因為緩存本身也是需要開銷的。如果每一個組件都用 memo 去包裹一下肛响,那么對瀏覽器的開銷就會很大岭粤,本末倒置了
- 項目中可以針對刷新頻率高的組件,根據(jù)實際情況特笋,使用 memo 進行優(yōu)化
useMemo:
- useMemo 是對計算的結(jié)果進行緩存剃浇,當緩存結(jié)果不變時,會使用緩存結(jié)果
- useMemo 并不是用的越多越好猎物,對于耗時長虎囚、性能開銷大的地方,可以使用 useMemo 來優(yōu)化蔫磨,但大多數(shù)情況下淘讥,計算結(jié)果的開銷還沒有使用 useMemo 的開銷大,應視情況而定
- 當父組件傳了一個引用類型的結(jié)果 result 給子組件堤如,且子組件用 memo 包裹時蒲列,需要使用 useMemo 對 result 進行緩存,因為 memo 只對 props 做淺比較煤惩,當父組件重新渲染時嫉嘀,會重新在內(nèi)存中開辟一個地址賦值給 result,此時地址發(fā)生改變魄揉,子組件會重新渲染
useCallback:
- useCallback 與 useMemo 類似剪侮,只不過是對函數(shù)進行緩存
- useCallback 可以單獨使用,但是單獨使用的使用對性能優(yōu)化并沒有實質(zhì)的提升,且父組件此時重新渲染瓣俯,子組件同樣會渲染
- useCallback 需要配合 memo 一起使用杰标,這樣當父組件重新渲染時,緩存的函數(shù)的地址不會發(fā)生改變彩匕,memo 淺比較會認為 props 沒有改變腔剂,因此子組件不會重新渲染