useMemo與useCallback

useMemo與useCallback

useMemouseCallback都可緩存函數的引用或值,從更細的角度來說useMemo則返回一個緩存的值,useCallback是返回一個緩存函數的引用。

useMemo

useMemoTS定義可以看出,範型TuseMemo中是一個返回的值類型。

type DependencyList = ReadonlyArray<any>;

function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;

下面是useMemo的簡單示例,在ab的變量值不變的情況下,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在每次渲染時都會計算新的值。
eslinteslint-plugin-react-hooks中的exhaustive-deps規則可以在添加錯誤依賴時發出警告並給出修復建議。
相比較於useEffect看起來和VueWatch很像,但是思想方面是不同的,Vue是監聽值的變化而React是用以處理副作用。在useMemo方面就和Vuecomputed非常類似了,同樣都屬於緩存依賴項的計算結果,當然在實現上是完全不同的。

useCallback

useCallbackTS定義可以看出,範型TuseCallback中是一個返回的函數類型。

type DependencyList = ReadonlyArray<any>;

function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

下面是useCallback的簡單示例,在ab的變量值不變的情況下,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的語法糖。
eslinteslint-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} />;
}

最後

關於useMemouseCallback是否值得儘量多用,私認爲並不應該這麼做,如果在性能優化方面非常有效,值得在每個依賴或者函數都值得使用useMemouseCallback的話,React可以乾脆將其作爲默認的功能,又可以減少用戶使用Hooks的心智負擔,又可以減少使用Hooks的包裹讓代碼更加簡潔,可是React並沒有這麼做,實際上這仍然是一個權衡的問題,權衡性能優化的點,取一個折衷,具體來說就是你需要評估你組件re-render 的次數和代價,React.memouseMemouseCallback這些緩存機制也是有代價的,需要做好平衡,不能盲目的多用這類緩存優化方案,比起盲目的進行各種細微的優化,分析清楚性能問題出現的原因才能真正的解決問題。

每日一題

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章