如何更優(yōu)雅的使用 useCallback 和 useMemo
以下文章來(lái)源于freeCodeCamp ,作者PapayaHUANG
我們都希望構(gòu)建強(qiáng)大的應(yīng)用,避免不必要的渲染。有一些鉤子可以幫助你實(shí)現(xiàn)這個(gè)愿望,但你可能不確定鉤子的選擇和使用時(shí)機(jī)。
我們將通過(guò)本文學(xué)習(xí) useCallback 和 useMemo的區(qū)別,以及如何衡量在代碼中使用它們的收益。
在我們開(kāi)始之前,請(qǐng)注意以下用于優(yōu)化 React 的方法實(shí)際上是不得已的選擇。代碼本身可能有許多改進(jìn)空間,在改進(jìn)代碼前,本文性能提升技巧可能還派不上用場(chǎng)。
但了解這些工具,以及知道如何使用它們很有必要。
幫助你理解文章的資料
useCallback 和 useMemo 的 Beta 版本官方文檔
示例項(xiàng)目源碼
示例項(xiàng)目 Demo
與往常一樣,我提供了一個(gè)示例項(xiàng)目,以便你在簡(jiǎn)化的環(huán)境中測(cè)試本文說(shuō)明的所有內(nèi)容。示例項(xiàng)目是對(duì)你將要學(xué)習(xí)要點(diǎn)的總結(jié)。
在開(kāi)始比較這兩個(gè)鉤子之前,讓我們回顧一些必要的背景概念。
什么是引用相等(Referential Equality)
當(dāng) React 對(duì)比 useEffect、 useCallback的依賴(lài)數(shù)組的值,或者傳入子組件的 props 值時(shí),使用的是 Object.is()。
詳細(xì)介紹可以查看 Object.is,簡(jiǎn)言之:
原始值是相等的(上文鏈接有少數(shù)例外)。
非原始值指向內(nèi)存中相同的對(duì)象。
譯者注:原始值指的是數(shù)據(jù)類(lèi)型為基本數(shù)據(jù)類(lèi)型(如:number、string、boolean 等)時(shí),兩個(gè)值相等的數(shù)據(jù)在嚴(yán)格模式下(===)也是相等的。非原始值值的數(shù)據(jù)類(lèi)型是引用類(lèi)型(如:object),由于引用類(lèi)型存儲(chǔ)的是對(duì)象的引用,所以只有當(dāng)兩個(gè)對(duì)象引用相同的底層對(duì)象,它們?cè)趪?yán)格模式下才是相等的。這種比較方式被稱(chēng)為“引用相等”。
簡(jiǎn)化示例如下:
"string" === "string" // true
0 === 0 // true
true === true // true
{} === {} // false
[] === [] // false
const f = () => 'Hi'
const f1 = f
const f2 = f
f1 === f1 // true
f1 === f2 // true
React.memo 的運(yùn)行機(jī)制
我將簡(jiǎn)單說(shuō)明一下React.memo的運(yùn)行機(jī)制(后文也會(huì)講解)。你可以在合適的時(shí)候使用它來(lái)提升性能。
當(dāng)想要避免子組件不必要的重新渲染(即便父組件發(fā)生了更改),你可以使用 React.memo 打包子組件 – 只要 props 不發(fā)生改變,就不會(huì)重復(fù)渲染。請(qǐng)注意此處是引用相等(譯者注:沿用了舊版本 React 的“淺比較”)——子組件不會(huì)被重新渲染。
import { memo } from 'react';
const ChildComponent = (props) => {
// ...
};
export default memo(ChildComponent);
現(xiàn)在你知道 React.memo 的運(yùn)行機(jī)制,讓我們開(kāi)始應(yīng)用吧。
useCallback 的運(yùn)行機(jī)制
useCallback 是 React 用來(lái)優(yōu)化代碼的內(nèi)置鉤子之一。但正如你將看到的那樣,它并不是直接為性能提升設(shè)計(jì)的鉤子。
簡(jiǎn)單來(lái)說(shuō),useCallback 允許你在組件渲染之間保存函數(shù)定義。
import { useCallback } from 'react';
const params = useCallback(() => {
// ...
return breed;
}, [breed]);
使用方法很簡(jiǎn)單:
從 React 引入useCallback,因?yàn)樗莾?nèi)置鉤子。
打包你想要保存定義的函數(shù)。
像使用 useEffect一樣,傳入依賴(lài)數(shù)組,告訴 React 這些存儲(chǔ)的值(在這里是函數(shù)定義)何時(shí)更新。
需要注意的是函數(shù)定義部分。它存儲(chǔ)定義,而不是執(zhí)行本身,也不是結(jié)果——所以每次調(diào)用時(shí)都會(huì)執(zhí)行該函數(shù)。因此,不要使用這個(gè)鉤子避免冗長(zhǎng)的計(jì)算。
那么保存函數(shù)定義的好處在哪兒呢?
回到引用相等
如果使用的是函數(shù)本身,而不是返回值,那么在:
useEffect 等鉤子的依賴(lài)數(shù)組
子組件的 prop、上下文等
要實(shí)現(xiàn)渲染之間真正的相等,useCallback就得保存內(nèi)存中對(duì)同一個(gè)對(duì)象的的引用。
如果不使用這個(gè)鉤子,每一次渲染函數(shù)都會(huì)重新指向內(nèi)存中的另一個(gè)引用。即便使用React.memo打包子組件,React 也會(huì)認(rèn)為是不同的函數(shù)。
你可以通過(guò)示例項(xiàng)目測(cè)試這個(gè)行為。在沒(méi)有優(yōu)化的版本中,每一次在輸入框填寫(xiě)內(nèi)容都會(huì)引發(fā)子組件的副作用。
在示例中,沒(méi)有優(yōu)化的版本只會(huì)導(dǎo)致一個(gè)虛擬的渲染放緩和重新抓取圖片。但假設(shè)在一個(gè)大型的項(xiàng)目中,會(huì)導(dǎo)致客戶端執(zhí)行大量計(jì)算,或者服務(wù)器的巨大開(kāi)銷(xiāo)。
useMemo 是如何運(yùn)作的
這是今天的第二個(gè)內(nèi)置鉤子。你可以把這個(gè)鉤子當(dāng)作直接優(yōu)化的手段,因?yàn)樗鎯?chǔ)函數(shù)的結(jié)果,除非依賴(lài)數(shù)組發(fā)生變化,函數(shù)不會(huì)再次執(zhí)行。
由于它可以存儲(chǔ)函數(shù)的結(jié)果,防止在組件渲染之間重復(fù)執(zhí)行,因此你可以在兩種情況下使用此鉤子。
引用相等
和 useCallback 一樣,我們也可以通過(guò) useMemo 來(lái)實(shí)現(xiàn)引用相等——但這次是結(jié)果的相等。
如果函數(shù)的返回值類(lèi)型在渲染間會(huì)被當(dāng)作不同的值對(duì)待,如對(duì)象或者數(shù)組,你可以使用 useMemo 來(lái)實(shí)現(xiàn)引用相等。
import { useMemo } from 'react';
const params = useMemo(() => {
// ...
return { breed };
}, [breed]);
從上面例子我們可以得出這樣使用 useMemo:
由 React 引入 useMemo,因?yàn)樗莾?nèi)置鉤子。
打包你想要保存結(jié)果的函數(shù)。
像使用 useEffect 一樣,傳入依賴(lài)數(shù)組,告訴 React 這些存儲(chǔ)的值(在這里是函數(shù)的返回值)何時(shí)更新。
在示例中,函數(shù)返回一個(gè)對(duì)象。通過(guò) Object.is 我們得知對(duì)象是不相等的,因?yàn)樗鼈兇鎯?chǔ)了不同的內(nèi)存地址。但是useMemo可以保存相同的引用。
你可以像之前一樣在示例項(xiàng)目中測(cè)試這個(gè)行為。在未優(yōu)化版本中,每一次按下鍵盤(pán),都會(huì)重新檢索圖片。使用 useMemo后,相等的返回值被保持,子組件不在重新檢索圖片。
昂貴的計(jì)算
由于使用 useMemo保存了值,避免函數(shù)重復(fù)執(zhí)行,所以我們可以使用它避免不必要的昂貴計(jì)算,提高網(wǎng)站的性能。
讓我們查看示例項(xiàng)目:
有一個(gè)組件,給定一個(gè)數(shù)字 n,就會(huì)打印出第 n 個(gè)斐波那契數(shù)。但是算法采用的遞歸版本性能很差。
你會(huì)發(fā)現(xiàn)有一個(gè)常量不斷被重復(fù)渲染。示例中的性能標(biāo)尺(Performance Gauge)會(huì)改變 state(每秒添加或者刪除方塊 60 次)。由于 state 一直發(fā)生改變,所以計(jì)算斐波那契數(shù)的函數(shù)也在重復(fù)執(zhí)行,即便給定的數(shù)字是一樣的。
在這種情況下,當(dāng)你在非優(yōu)化版本中使用更大的數(shù)字,就會(huì)發(fā)現(xiàn)性能肉眼可見(jiàn)的下降。優(yōu)化版本只會(huì)在你更改滑塊中的數(shù)字(更改給定數(shù)字)時(shí)出現(xiàn)性能峰值,但其余渲染將跳過(guò)計(jì)算并直接提供結(jié)果。
這里的問(wèn)題是,在我們的日常工作中,不會(huì)遇到可以稱(chēng)為“昂貴計(jì)算”的計(jì)算,使用 useMemo 的決定不一定是“總是”或“從不”。
何時(shí)優(yōu)化
到目前為止,你已經(jīng)了解了通過(guò)一些指標(biāo)確定何時(shí)使用不同的鉤子來(lái)避免不必要的渲染和/或副作用?,F(xiàn)在讓我們定義一些通用規(guī)則來(lái)決定在那些不太清楚的情況下到底是否使用這些鉤子:
回顧你的代碼,重新思考代碼構(gòu)建。你會(huì)發(fā)現(xiàn)最能提升性能的其實(shí)是你代碼本身。更多信息可以查看 Dan Abramov 的這篇博文。
如果不能證明優(yōu)化可以帶來(lái)好處,就不要優(yōu)化——優(yōu)化也有成本。
如果你不希望做額外的工作來(lái)證明優(yōu)化可以帶來(lái)好處,那請(qǐng)誠(chéng)實(shí)對(duì)待自己的內(nèi)心:其實(shí)你也不想優(yōu)化。
如何衡量性能影響/收益
最重要的優(yōu)化規(guī)則(總是在檢查代碼之后再使用)是能夠衡量更改是否生效以及增益百分比是多少。你這樣做不僅是為了可以在下一次績(jī)效評(píng)估提高相應(yīng)的百分比。
當(dāng)你懷疑存在性能問(wèn)題或只是想檢查代碼可以改進(jìn)部分時(shí),有以下兩種選擇:
笨拙的方法
我把這個(gè)方法也納入到文章中來(lái),是因?yàn)樽屛覀兠鎸?duì)現(xiàn)實(shí)吧:你一直在到處使用 console.log 調(diào)試代碼,不是嗎?別擔(dān)心,我和你一樣。
嘗試衡量性能問(wèn)題的一種快速方法是找出執(zhí)行某個(gè)動(dòng)作需要多長(zhǎng)時(shí)間以及該動(dòng)作執(zhí)行了多少次。因此,可以這么做:
const t0 = performance.now();
expensiveCalculation(targetNumber);
const t1 = performance.now();
console.log(`Call to expensiveCalculation took ${t1 - t0} milliseconds.`);
console.count('Expensive Calculation');
但這種方法只能檢測(cè)出一些你已經(jīng)懷疑的非常明顯的情況。
同時(shí)請(qǐng)小心 StrictMode,出于穩(wěn)定性考慮,它可能會(huì)導(dǎo)致 console.count 重復(fù)渲染。
現(xiàn)在讓我們查看正確的方法。
專(zhuān)業(yè)的方法
在這個(gè)方法中,你將使用官方的 React 開(kāi)發(fā)者工具來(lái)檢查代碼片段的性能。一旦你在瀏覽器添加了這個(gè)擴(kuò)展程序,你就可以打開(kāi)瀏覽器,搜索 Profiler。
我將通過(guò)示例項(xiàng)目演示,你也可以在你自己的項(xiàng)目中測(cè)試。
當(dāng)你點(diǎn)擊 record 按鈕,然后開(kāi)始進(jìn)行你認(rèn)為需要關(guān)注性能的一些行為,profiler 就會(huì)保存并且打印出這個(gè)過(guò)程具體發(fā)生的細(xì)節(jié)和解釋。
如在昂貴計(jì)算項(xiàng)目中,我們對(duì)比的沒(méi)有優(yōu)化和 useMemo 版本的結(jié)果:
在兩個(gè)版本中,我分別點(diǎn)擊 record 按鈕,等待幾秒鐘,再次點(diǎn)擊 record 按鈕獲取結(jié)果。如你所見(jiàn),在我們準(zhǔn)備的極端案例中,可以見(jiàn)到巨大的性能提升。
讓我們仔細(xì)觀察 profiler 中發(fā)生了什么:
灰色條目是在渲染間沒(méi)有發(fā)生變化的組件,所以不用擔(dān)心性能方面的問(wèn)題。
綠色和黃色條目是發(fā)生變化的組件,你可以看到渲染需要多長(zhǎng)時(shí)間。
如果你點(diǎn)擊每一個(gè)條目,可以看到更多的解釋信息和數(shù)據(jù)。
我之后會(huì)出一篇文章詳細(xì)介紹 profiler,但是現(xiàn)在讓我們看幾個(gè)使用小技巧:
在 settings 圖標(biāo) General 菜單下,勾選 Highlight updates when components render.。這將顯示渲染的內(nèi)容,并可以檢測(cè)在某些操作下不被渲染的子組件。
在 settings 圖標(biāo) Profiler 菜單下,勾選 Record why each component rendered while profiling. 這將對(duì)正在渲染的組件內(nèi)容的添加簡(jiǎn)要說(shuō)明,或許能幫助你找到哪里需要提升。
總結(jié)
如你所見(jiàn),這兩個(gè)常被誤解的鉤子是完全不同的函數(shù),使用的場(chǎng)景也不太相同?,F(xiàn)在你可以檢查你現(xiàn)在或者過(guò)去的項(xiàng)目,來(lái)看看是否誤用了這些鉤子。
在未來(lái) React 或許能夠自動(dòng)完成優(yōu)化。但在撰寫(xiě)本文時(shí),優(yōu)化仍是一個(gè)應(yīng)該謹(jǐn)慎對(duì)待并經(jīng)過(guò)全面分析的過(guò)程。
我希望你覺(jué)得這篇教程有用,能幫助你使用 React 構(gòu)建性能更好的應(yīng)用程序。謝謝你閱讀本文!
原文鏈接:https://www.freecodecamp.org/news/better-react-performance-usecallback-vs-usememo/
作者:Daniel Asta
譯者:PapayaHUANG
歡迎關(guān)注微信公眾號(hào) :前端快快跑