引言
自從react hooks出現(xiàn)以來(lái)像屋,越來(lái)越多的人或者團(tuán)隊(duì)選擇使用react hooks,很多人都覺(jué)得useCallback是解決性能問(wèn)題的一大利器,但你真的用對(duì)了么项炼?
下面就是筆者在實(shí)踐中得出在具體場(chǎng)景中如何使用好useCallback來(lái)提高性能的結(jié)論。
背景知識(shí)
說(shuō)起useCallback為什么可以解決性能問(wèn)題示绊,就涉及到re-render問(wèn)題了锭部,眾所周知在react中父組件的re-render會(huì)引發(fā)子組件的re-render,但有時(shí)候的re-render其實(shí)是不必要的耻台。例如:父組件并未傳遞props給子組件空免,渲染結(jié)果不變。
運(yùn)行以下案例可以發(fā)現(xiàn)input輸入內(nèi)容后盆耽,觸發(fā)setState蹋砚,從而觸發(fā)Case1組件的re-render,當(dāng)父組件re-render時(shí)摄杂,子組件A也會(huì)發(fā)生re-render坝咐。當(dāng)你每次輸入input內(nèi)容,都會(huì)在控制臺(tái)中看到有render_A的log析恢。
// case1
class A extends React.Component {
// A 父組件的count變化時(shí)墨坚,A組件會(huì)不斷的re-render
render() {
console.log("render_A");
return <div>這是A組件</div>;
}
}
export default function Case1() {
const [count, setCount] = useState(0);
const onChange = (data) => {
setCount(data.target.value);
};
return (
<>
<input value={count} onChange={onChange} />
<A />
</>
);
}
useCallback
如何使用useCallback來(lái)解決子組件re-render的問(wèn)題
以上的案例說(shuō)明了新能浪費(fèi)的原因,那么要如何使用useCallback來(lái)解決子組件re-render的問(wèn)題呢映挂?
useCallback在官方文檔是這么解釋的:
Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
useCallback有2個(gè)參數(shù)泽篮,第一個(gè)是inline的callback函數(shù),第二個(gè)是依賴項(xiàng)數(shù)組柑船。使用useCallback在依賴項(xiàng)發(fā)生變更時(shí)將會(huì)返回一個(gè)callback函數(shù)的memoized版本帽撑。當(dāng)你把callback函數(shù)傳遞給經(jīng)過(guò)子組件時(shí),如果使用了useCallback會(huì)因?yàn)閜rops的相等性而避免了非必要的渲染鞍时。
那么在實(shí)際使用中真的用對(duì)了方式么亏拉?
錯(cuò)誤使用案例1
看以下例子,子組件A的回調(diào)函數(shù)已經(jīng)使用了useCallback逆巍,但是當(dāng)你通過(guò)input改變count的值時(shí)及塘,子組件A還是在不斷的re-render。
// case2
const A = ({ onClick }) => {
// A 父組件的count變化時(shí)锐极,A組件仍舊會(huì)不斷的re-render
console.log("case2: render_A");
return <button onClick={onClick}>A組件+count</button>;
};
export default function Case2() {
const [count, setCount] = useState(0);
const onClick = useCallback(() => {
setCount((count) => count + 1);
}, []);
return (
<>
<p>count:{count}</p>
<A onClick={onClick} />
</>
);
}
以上案例為什么沒(méi)有避免無(wú)效的re-render呢吮蛹?
是因?yàn)楹瘮?shù)式組件要避免re-render,還需要結(jié)合React.memo來(lái)使用敌蚜。使用高階組件React.memo來(lái)包裹函數(shù)式組件碗脊,它和類組件的PureComponent類似,也是對(duì)props進(jìn)行淺比較(根據(jù)內(nèi)存地址判斷)決定是否更新庇勃。
在函數(shù)組件中,函數(shù)作為props傳遞給子組件時(shí)槽驶,無(wú)論子組件是pureComponent還是用React.memo進(jìn)行包裹责嚷,都會(huì)讓子組件render,而配合useCallback使用就能讓子組件不隨父組件render掂铐。
上面案例修改一下罕拂,如下就不會(huì)發(fā)生re-render
const A = ({ onClick }) => {
// A 父組件的count變化時(shí),A組件不會(huì)re-render
console.log("case2: render_A");
return <button onClick={onClick}>A組件+count</button>;
};
const B = React.memo(A);
export default function Case2() {
const [count, setCount] = useState(0);
const onClick = useCallback(() => {
setCount((count) => count + 1);
}, []);
return (
<>
<p>count:{count}</p>
<B onClick={onClick} />
</>
);
}
使用useCallback全陨,dependencies要列清楚
為什么說(shuō)使用useCallback爆班,dependencies要列清楚呢,先來(lái)看以下案例:
錯(cuò)誤使用案例2
看下面的例子辱姨,在子組件B的回調(diào)函數(shù)中柿菩,使用了useCallback,但是沒(méi)有添加任何的dependencies雨涛,那么onClick useCallback回調(diào)函數(shù)中count的值永遠(yuǎn)都是初始值0枢舶。在input中改變了值后點(diǎn)擊A組件,在console中展示效果永遠(yuǎn)都是1替久。
const A = ({ onClick }) => {
// A 父組件的count變化時(shí)凉泄,A組件不會(huì)re-render
console.log("case2: render_A");
return <button onClick={onClick}>A組件+count</button>;
};
const B = React.memo(A);
export default function Case2() {
const [count, setCount] = useState(0);
const onClick = useCallback(() => {
console.log(count + 1); // 此處的count一直都是0
}, []);
return (
<>
<p>count:{count}</p>
<B onClick={onClick} />
</>
);
}
把count作為dependencies加到useCallback中,在input中改變了值后點(diǎn)擊A組件蚯根,console中輸出就是當(dāng)前count的最新值后众。所以在使用useCallback時(shí),一定要把當(dāng)前的回調(diào)函數(shù)的dependencies梳理清楚颅拦,避免值沒(méi)更新導(dǎo)致的bug蒂誉,例如分頁(yè)獲取數(shù)據(jù)的時(shí)候,永遠(yuǎn)獲取的是第一頁(yè)的數(shù)據(jù)等距帅。
【當(dāng)前案例只用來(lái)說(shuō)明dependencies正確的重要性】
const A = ({ onClick }) => {
// A 父組件的count變化時(shí)右锨,A組件會(huì)不斷的re-render
console.log("case2: render_A");
return <button onClick={onClick}>A組件+count</button>;
};
const B = React.memo(A);
export default function Case2() {
const [count, setCount] = useState(0);
const onChange = (data) => {
setCount(data.target.value);
};
const onClick = useCallback(() => {
console.log(count + 1);
}, [count]);
return (
<>
<p>count:{count}</p>
<input value={count} onChange={onChange} />
<B onClick={onClick} />
</>
);
}
添加dependencies后,當(dāng)dependencies變化時(shí)會(huì)導(dǎo)致子組件隨著父組件re-render锥债。
所以在具體使用中陡蝇,如果導(dǎo)致父組件re-render的因素又同時(shí)全都是子組件useCallback的dependencies的話痊臭,就不必使用useCallback多此一舉了哮肚,反正都要跟著父組件一起render的。就像上面這個(gè)case一樣广匙。
如何從 useCallback 讀取一個(gè)經(jīng)常變化的值的方法可以查看官方文檔:英文版,中文版
如果觸發(fā)父組件的render因素很多允趟,但是觸發(fā)子組件的因素很少的話,就盡可能使用useCallback+React.memo來(lái)減少子組件的render次數(shù)鸦致。
【使用dependencies注意事項(xiàng)】 使用useEffect時(shí)潮剪,dependencies是非純函數(shù)涣楷,使用useCallback時(shí)要注意避免死循環(huán)。在實(shí)踐過(guò)程中最容易出現(xiàn)的一種死循環(huán)就是非純函數(shù)中請(qǐng)求了分頁(yè)的數(shù)據(jù)抗碰,set到State中狮斗,然后又把非純函數(shù)作為useEffect的dependencies,那么setState后re-render弧蝇,re-render導(dǎo)致的非純函數(shù)又是新的instance碳褒,作為依賴項(xiàng)就又會(huì)變調(diào)用,因此陷入死循環(huán)看疗。
useMemo
如果是組件中有復(fù)雜計(jì)算的function沙峻,應(yīng)該使用usememo而不是useCallback。因?yàn)閡seCallback緩存函數(shù)的引用两芳,useMemo緩存計(jì)算數(shù)據(jù)的值摔寨。useMemo是避免在每次渲染時(shí)都進(jìn)行高開(kāi)銷的計(jì)算的優(yōu)化的策略.
useMemo需要傳入兩個(gè)參數(shù),第一個(gè)參數(shù)是callback(回調(diào)函數(shù))怖辆,并把要邏輯處理函數(shù)放在callback內(nèi)執(zhí)行(該函數(shù)需要有返回值)是复,第二個(gè)參數(shù)是dependencies,和useCallback/useEffect一樣是引入的外部參數(shù)或者是依賴參數(shù)疗隶。
useMemo 返回一個(gè) memoized 值佑笋。在依賴參數(shù)不變的的情況返回的是上次第一次計(jì)算的值,當(dāng)依賴參數(shù)發(fā)生變化時(shí)useMemo就會(huì)自動(dòng)重新計(jì)算返回一個(gè)新的 memoized值斑鼻。
使用案例如下:
const memoizedValue = useMemo(() => calculateFunc(a, b), [a, b]);
在a和b的變量值不變的情況下蒋纬,memoizedValue的值不變。即:useMemo函數(shù)的第一個(gè)入?yún)⒑瘮?shù)不會(huì)被執(zhí)行坚弱,從而達(dá)到節(jié)省計(jì)算量的目的蜀备。
結(jié)尾
以上就是關(guān)于useCallback和useMemo的具體使用方式,也通過(guò)案例解釋了為什么使用這兩者可以達(dá)到性能優(yōu)化的目的荒叶。