avatarHannah Lin

总结

该网页是一篇关于 React Hooks 的深入学习文章,专注于介绍 Memorized Hooks,包括 useMemouseCallback 的使用和区别,以及如何在 TypeScript 中对这些 Hooks 进行类型标注。

摘要

文章首先介绍了 React Hooks 的基础知识,并指出了网络上大多数文章只介绍 useStateuseEffect 的现象。作者提供了一个系列文章的索引,包括对 useStateuseEffectuseMemouseCallbackuseRefuseContextuseReduceruseLayoutEffect 的深入探讨。文章强调了 useMemouseCallback 的重要性,它们可以帮助开发者避免在每次渲染时执行昂贵的计算,从而提高应用性能。

文章详细解释了 useMemouseCallback 的工作原理,并通过示例代码展示了它们的应用场景。useMemo 用于存储计算结果,而 useCallback 用于存储回调函数,两者都依赖于依赖数组来决定何时重新计算或重新创建回调函数。文章还指出了这些 Hooks 的不同之处,例如 useCallback 返回的是一个记忆化的回调函数,而 useMemo 返回的是记忆化的值。

此外,文章还讨论了在不适当的情况下使用 Memorized Hooks 可能导致的性能问题,并提供了一些最佳实践,如何在 TypeScript 中正确地为这些 Hooks 添加类型标注。最后,作者提供了一些参考资源,包括官方文档和相关视频教程,供读者进一步学习。

观点

  • useMemouseCallback 的主要作用是优化性能,通过避免在每次渲染时重新执行昂贵的计算和创建函数实例。
  • 适用场景:当函数执行速度慢且结果不需要频繁变动,且是纯函数时,使用 useMemouseCallback 特别有效。
  • 记忆化的依赖性:这两个 Hooks 都依赖于依赖数组,只有当依赖项发生变化时,才会重新计算值或创建新的函数实例。
  • 不同之处useCallback 返回一个记忆化的回调函数,useMemo 返回一个记忆化的值。
  • 类型标注:在 TypeScript 中,正确地为 useCallbackuseMemo 添加类型标注,可以提高代码的可读性和可维护性。
  • 性能考虑:不建议盲目地对所有函数使用记忆化 Hooks,因为不当使用可能会导致额外的内存开销和性能问题。
  • 最佳实践:只有在确实需要优化性能的情况下才应使用 useMemouseCallback,并且应该尽量减少依赖数组中的元素,以避免不必要的重渲染。

[React Hook 筆記] Memorized Hook- useMemo, useCallback

把東西用 useMemo/useCallback 存起來就不用每次重新 render 拖慢效能

(新增 typing 於 2023/11/7)

React Hook 系列文

1. 從最基本的 Hook 開始 useState, useEffect 2. Memorized Hook- useMemo, useCallback 3. useRef 4. useContext 5. useReducer 6. useLayoutEffect 7. Custom Hooks

圖改編自 kevinwkds.medium.com

至從 React Hook 興起,網路上多數文章都只介紹 useStateuseEffect,但明明 Hook 還有很多別的,所以這篇想先從比較少看到的 Memorized Hook 開始。若 useStateuseEffect 都很不熟可以先閱讀 從最基本的 Hook 開始 useState, useEffect 再回來看此篇

🔖 文章索引
1. Memorized Hook 在幹嘛?
2. useMemo
3. useCallback
4. useCallback 跟 useMemo 有什麼不同
5. Typing useCallback/useMemo
6. 結論

Memorized Hook 在幹嘛?

顧名思義就是把東西存起來就不用每次都重新 render, 最常使用的情境就是當 function 符合以下三點

  • 執行速度很慢 (Expensive function or expensive calculation)
  • 不需要常常再被 render over and over again,變動性不大
  • Pure function (每次被呼叫 output 都會一樣)
// 此函式就符合以上三點
function slowFunc(num) {
  // 執行速度很慢
  for (let i=0; i<=1000000; i++) {}
  return num*2;
}
const doubleNum = slowFunc(1); // 2

若執行 slowFunction 需要 10 秒鐘,那執行三次就需要 30 秒; 但若先存起來,那只有第一次需要 10 秒,之後若需要他,直接從名為 doubleNum抽屜拿已經存起來的值 2 就好了

不過只要這函式不是 pure function 或是會產生 side effect 就不適合用 memoize

/* 現在時間是一直變動的,並不是 Pure function (one input, one output) */
const getCurrentTimeMemoized = memoize(Date.now);

getCurrentTimeMemoized(); // 1683784131157
getCurrentTimeMemoized(); // 1683784131157
getCurrentTimeMemoized(); // 1683784131157 incorrect 


/* 上傳資料會產生 side effect,每次上傳資料可能都不同,所以也不適合使用 memoize */
function uploadRow(row) {
  // upload logic
}

const memoizedUpload = memoize(uploadRows);
memoizedUpload('Some Data'); // successful upload
memoizedUpload('Some Data'); // nothing happens

另外很常搞混 Memorized Hook 的 useMemouseCallback,這邊先講結論下面會再詳細比較

useCallback(fn, deps) 等同於 useMemo(() => fn, deps)

Note. 其實在 Hook 出現前已經有一些方法來 memorized

  • createSelector (Create a selector for redux state and memorizes the result)
  • React.memo (Memoizes a React functional component based on its props)

useMemo

Returns a memoized value. 也就是 dependencies 沒有改變的情況下,把某個運算的保存下來 ( 這個 slowFunction 回傳值可以是 object、array、basic type)

const memoizedValue = useMemo(() => slowFunction(a, b), [a, b]);
https://codepen.io/hannahpun/pen/ExgRbLo?editors=1011

用法 1: 暫存起來

把執行速度慢且不需要常常再被 call 的函式結果存起來。直接來看範例比較容易,以下是一個非常簡單的 React Component

setNumber 會觸發 re-render App 這個 component,所以可以預想每一次 slowFunction 都會被重新呼叫並執行

這其實是非常影響效能的一件事,因為 slowFunction 不但執行速度慢而且明明每次回傳值都ㄧ樣但卻要一直重覆被執行。為了提高效能,可以把他的值先存起來

const doubleNumber = useMemo(() => { 
  return slowFunction(number)
}, [number])

useMemo 第一個參數是放函式,第二個參數是該 Memo 所依賴的值 array,意思跟 useEffect 第二個依賴 Array 參數一樣,有變動的話才會重新 render 此函式

以上可以看到 doubleNumber 是依賴 number,所以只有當 number 有變動時才會重新執行 slowFunction ,當 setDark時並不會 re-render doubleNumber,也就是不會 re-call slowFunction因為我們知道他的值就是 last time 回傳的值; 反之 themeStyle 裡函示也是一樣概念

猜猜若沒有包進 useMemo 那 console 出來會是什麼呢?

"Calling Slow Function"
"Theme Change"
"Calling Slow Function"
"Theme Change"
"Calling Slow Function"
"Theme Change"

用法 2: 解決 {} != {}

大家知道 javaScript 的函式也是物件的一種,然後物件是 by reference 所以

const a = {name: 'Hannah'};
const b = {name: 'Hannah'};
console.log(a === b); // false

有了這個觀念再來看以下例子

const App = () => {
  const [dark, setDark] = useState(true);
  const themeStyle = {
    backgroundColor: dark ? '#2c3e50': '#ecf0f1',
    color: dark ? '#ecf0f1' : '#2c3e50'
  }
  
  useEffect(() => {
    console.log('Theme Change')
  }, [themeStyle])
}

你會以為只要 themeStyle 沒變動,就不會一直印出 Theme Change ?

其實 js 會認為每一次 themeStyle 回傳的 Object 都是不相等的,所以才會不斷重新 render。 這時候就可以把 Object 包在 memo 裡,這樣 js 就會認得它是同一個 reference,就不會重新 re-render

const App = () => {
const themeStyle = useMemo(() => {
    return {
      backgroundColor: dark ? '#2c3e50': '#ecf0f1',
      color: dark ? '#ecf0f1' : '#2c3e50'
    }
  }, [dark])
  
  useEffect(() => {
    console.log('Theme Change')
  }, [themeStyle])
}

要謹記傳到 useMemo 的 function 會在 render 期間執行。不要做一些通常不會在 render 期間做的事情。例如,處理 side effect 屬於 useEffect,而不是 useMemo

如果沒有提供 array,每次 render 時都會計算新的值。

處理 side effect 屬於 useEffect,而不是 useMemo。

useCallBack

Returns a memoized callback. 也就是 dependencies 沒有改變的情況下,把某個 function 保存下來

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
https://codepen.io/hannahpun/pen/yLaRyvM

會發現其實 useCallbackuseMemo 用法真的大同小異

useCallback 跟 useMemo 有什麼不同

  • useCallback 回傳 callBack function,所以可以傳參數進去
  • useMemo 回傳值

useCallback(fn, deps) 等同於 useMemo(() => fn, deps)

Typing useCallback/useMemo

加 type 進 useCallback/useMemo 沒什麼困難度,只要記得

useCallback 回傳 function,useMemo 回傳值

// return type is number
const doubleNumber = useMemo<number>(() => {  
  return slowFunction(number)
}, [number])


// return is a function
const doubleNumber = useCallback((arg: string) => {  
  return slowFunction(number)
}, [number])


//error: Type number does not satisfy to contraint 'Function'
const doubleNumber = useCallback<number>(() => {  
  return slowFunction(number)
}, [number])

結論

既然 Memorized Hook 可以節省效能那乾脆所有函式都包進去不就完美?! 其實不建議全部都用 Memorized Hook,因為可能會有 memory overhead, memory usage 太多也是會有 performance issue 的,例如若頻繁需要 render 的就不建議使用,所以記得適用的函式要符合

  • 執行速度很慢
  • 不需要常常再被 render over and over again,變動性不大

Reference

不得不說 React 官網已經寫得相當好,這邊也參考以下

React Hook
React
Recommended from ReadMedium