useMemo與useCallback
useMemo
和useCallback
都可緩存函數的引用或值,從更細的角度來說useMemo
則返回一個緩存的值,useCallback
是返回一個緩存函數的引用。
useMemo
useMemo
的TS
定義可以看出,範型T
在useMemo
中是一個返回的值類型。
type DependencyList = ReadonlyArray<any>;
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
下面是useMemo
的簡單示例,在a
和b
的變量值不變的情況下,memoizedValue
的值不變,在此時useMemo
函數的第一個參數也就是computeExpensiveValue
函數不會被執行,從而達到節省計算量的目的。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
把創建函數factory: () => T
和依賴項數組deps: DependencyList | undefined
作爲參數傳入 useMemo
,它僅會在某個依賴項改變時才重新計算memoized
值,這種優化有助於避免在每次渲染時都進行高開銷的計算,例如上文的computeExpensiveValue
是需要一個大量計算的函數時,useMemo
有助於減少性能開銷,以防止Js
太多次長時間運行計算導致頁面無響應。
此外,傳入useMemo
的函數會在渲染期間執行,所以不要在這個函數內部執行與渲染無關的操作,諸如副作用這類的操作屬於 useEffect
的適用範疇,而不是useMemo
。如果沒有提供依賴項數組,useMemo
在每次渲染時都會計算新的值。
eslint
的eslint-plugin-react-hooks
中的exhaustive-deps
規則可以在添加錯誤依賴時發出警告並給出修復建議。
相比較於useEffect
看起來和Vue
的Watch
很像,但是思想方面是不同的,Vue
是監聽值的變化而React
是用以處理副作用。在useMemo
方面就和Vue
的computed
非常類似了,同樣都屬於緩存依賴項的計算結果,當然在實現上是完全不同的。
useCallback
useCallback
的TS
定義可以看出,範型T
在useCallback
中是一個返回的函數類型。
type DependencyList = ReadonlyArray<any>;
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
下面是useCallback
的簡單示例,在a
和b
的變量值不變的情況下,memoizedCallback
的函數引用不變,在此時useCallback
函數的第一個參數不會被重新定義,即引用的依舊是原函數,從而達到性能優化的目的。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
把內聯回調函數callback: T
及依賴項數組deps: DependencyList
作爲參數傳入 useCallback
,它將返回該回調函數的memoized
版本,該回調函數僅在某個依賴項改變時纔會更新,將回調函數傳遞給經過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子組件時,它將非常有用。此外,useCallback(fn, deps)
相當於useMemo(() => fn, deps)
,由此useCallback
可以看作useMemo
的語法糖。
eslint
的eslint-plugin-react-hooks
中的exhaustive-deps
規則可以在添加錯誤依賴時發出警告並給出修復建議。
在useCallback
的應用方面,在這裏引用一下 @鬆鬆 給出的例子,一般Js
上創建一個函數需要的時間並不至於要緩存的程度,那爲什麼要專門給緩存函數的創建做一個語法糖呢,這就跟React.memo
有關係了。React.memo
的默認第二參數是淺對比shallow compare
上次渲染的props
和這次渲染的props
,如果你的組件的props
中包含一個回調函數,並且這個函數是在父組件渲染的過程中創建的(見下例),那麼每次父組件(下例中的<MyComponent />
)渲染時,React
是認爲你的子組件(下例中的<Button />
)props
是有變化的,不管你是否對這個子組件用了React.memo
,都無法阻止重複渲染。這時就只能用useCallback
來緩存這個回調函數,纔會讓React
(或者說Js
)認爲這個prop
和上次是相同的。
// 下面三種方法都會在MyComponent渲染的過程中重新創建這個回調函數
// 這樣都會引起Button的重新渲染 因爲Button的props變化了
function MyComponent() {
return <Button onClick={() => doWhatever()} />;
}
function MyComponent() {
const handleClick = () => doWhatever();
return <Button onClick={handleClick} />;
}
function MyComponent() {
function handleClick(){
doWhatever();
}
return <Button onClick={handleClick} />;
}
// 只有使用useCallback, 纔會導致即使MyComponent渲染,也不重新創建一個新的回調函數
// 這樣就不會引發Button的重新渲染 因爲Button的props沒變
function MyComponent() {
const handleClick = React.useCallBack(() => doWhatever(), []);
return <Button onClick={handleClick} />;
}
最後
關於useMemo
與useCallback
是否值得儘量多用,私認爲並不應該這麼做,如果在性能優化方面非常有效,值得在每個依賴或者函數都值得使用useMemo
與useCallback
的話,React
可以乾脆將其作爲默認的功能,又可以減少用戶使用Hooks
的心智負擔,又可以減少使用Hooks
的包裹讓代碼更加簡潔,可是React
並沒有這麼做,實際上這仍然是一個權衡的問題,權衡性能優化的點,取一個折衷,具體來說就是你需要評估你組件re-render
的次數和代價,React.memo
、useMemo
與useCallback
這些緩存機制也是有代價的,需要做好平衡,不能盲目的多用這類緩存優化方案,比起盲目的進行各種細微的優化,分析清楚性能問題出現的原因才能真正的解決問題。
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://www.zhihu.com/question/428921970
https://www.zhihu.com/question/390974405
https://juejin.cn/post/6844904032113278990
https://juejin.cn/post/6844904001998176263
https://segmentfault.com/a/1190000039405417
https://www.infoq.cn/article/mm5btiwipppnpjhjqgtr
https://zh-hans.reactjs.org/docs/hooks-reference.html