詳解 useCallback & useMemo

yi個程式猿發表於2020-03-24

前言

閱讀本文章需要對 React hooksuseStateuseEffect 有基礎的瞭解。我的這篇文章內有大致介紹 在 React 專案中全量使用 Hooks

useCallback

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.

簡單來說就是返回一個函式,只有在依賴項發生變化的時候才會更新(返回一個新的函式)。

useCallback 的應用

線上程式碼: Code Sandbox

import React, { useState, useCallback } from 'react';
import Button from './Button';

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleClickButton1 = () => {
    setCount1(count1 + 1);
  };

  const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]);

  return (
    <div>
      <div>
        <Button onClickButton={handleClickButton1}>Button1</Button>
      </div>
      <div>
        <Button onClickButton={handleClickButton2}>Button2</Button>
      </div>
    </div>
  );
}
複製程式碼
// Button.jsx
import React from 'react';

const Button = ({ onClickButton, children }) => {
  return (
    <>
      <button onClick={onClickButton}>{children}</button>
      <span>{Math.random()}</span>
    </>
  );
};

export default React.memo(Button);
複製程式碼

在案例中可以點選 Button1 和 Button2 兩個按鈕來檢視效果,點選 Button1 的時候只會更新 Button1 後面的內容,點選 Button2 會將兩個按鈕後的內容都更新。這就表示我在點選 Button2 的時候導致了兩個按鈕內都重新渲染了。

這裡或許會注意到 React.memo 這個方法,此方法內會對 props 做一個淺層比較,如果如果 props 沒有發生改變,則不會重新渲染此元件。

const a = () => {};
const b = () => {};
a === b; // false
複製程式碼

上述程式碼可以看到我們兩個一樣的函式卻是不相等的(這是個廢話,我相信能看到這的人都知道,所以不做解釋了)。

const [count1, setCount1] = useState(0);
// ...
const handleClickButton1 = () => {
  setCount1(count1 + 1);
};
// ...
return <Button onClickButton={handleClickButton1}>Button1</Button>
複製程式碼

回頭再看上面的 Button 元件都需要一個 onClickButton 的 props ,儘管元件內部有用 React.memo 來做優化,但是我們宣告的 handleClickButton1 是直接定義了一個方法,這也就導致只要是父元件重新渲染(狀態或者props更新)就會導致這裡宣告出一個新的方法,新的方法和舊的方法儘管長的一樣,但是依舊是兩個不同的物件,React.memo 對比後發現物件 props 改變,就重新渲染了。

const handleClickButton2 = useCallback(() => {
  setCount2(count2 + 1);
}, [count2]);
複製程式碼

上述程式碼我們的方法使用 useCallback 包裝了一層,並且後面還傳入了一個 [count2] 變數,這裡 useCallback 就會根據 count2 是否發生變化,從而決定是否返回一個新的函式,函式內部作用域也隨之更新。

由於我們的這個方法只依賴了 count2 這個變數,而且 count2 只在點選 Button2 後才會更新 handleClickButton2,所以就導致了我們點選 Button1 不重新渲染 Button2 的內容。

注意點

import React, { useState, useCallback } from 'react';
import Button from './Button';

export default function App() {
  const [count2, setCount2] = useState(0);

  const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, []);

  return (
    <Button 
      count={count2}
      onClickButton={handleClickButton2}
    >Button2</Button>
  );
}
複製程式碼

我們調整了一下程式碼,將 useCallback 依賴的第二個引數變成了一個空的陣列,這也就意味著這個方法沒有依賴值,將不會被更新。且由於 JS 的靜態作用域導致此函式內 count2 永遠都 0

可以點選多次 Button2 檢視變化,會發現 Button2 後面的值只會改變一次。因為上述函式內的 count2 永遠都是 0,就意味著每次都是 0 + 1,Button 所接受的 count props,也只會從 0 變成 1且一直都將是 1,而且 handleClickButton2 也因沒有依賴項不會返回新的方法,就導致 Button 只會因 count 改變而更新一次後就不會被重新渲染。

useMemo

useMemo 的作用

官方文件:

Pass a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed.

簡單來說就是傳遞一個建立函式和依賴項,建立函式會需要返回一個值,只有在依賴項發生改變的時候,才會重新呼叫此函式,返回一個新的依賴值。

useMemo 的應用

useMemo 與 useCallback 很像,根據上述 useCallback 已經可以想到 useMemo 也能針對傳入子元件的值進行快取優化,當然這個值必須是一個物件,如果不是物件而是一些簡單型別的如字串等,那麼沒更改 React.memo 也能對比出來,下面就直接舉個 ? 對比一下。

// ...
const [count, setCount] = useState(0);

const userInfo = {
  // ...
  age: count,
  name: 'Jace'
}

return <UserCard userInfo={userInfo}>
複製程式碼
// ...
const [count, setCount] = useState(0);

const userInfo = useMemo(() => {
  return {
    // ...
    name: "Jace",
    age: count
  };
}, [count]);

return <UserCard userInfo={userInfo}>
複製程式碼

很明顯的上面的 userInfo 每次都將是一個新的物件,無論 count 發生改變沒,都會導致 UserCard 重新渲染,而下面的則會在 count 改變後才會返回新的物件。

實際上 useMemo 的作用不止於此,根據官方文件內介紹,它主要的功能應該是:

This optimization helps to avoid expensive calculations on every render.

可以吧一些昂貴的計算邏輯放到 useMemo 中,只有當依賴值發生改變的時候才去更新。

const num = useMemo(() => {
  let num = 0;
  // 這裡使用 count 針對 num 做一些很複雜的計算,當 count 沒改變的時候,元件重新渲染就會直接返回之前快取的值。
  return num;
}, [count]);

return <div>{num}</div>
複製程式碼

也能在很多情況將兩種情況結合起來用。

結束。