React 優化: 到底怎么用 useCallback 才是正確的?


前言

之前在 React Hook 相關內容 中跟大家提過諸如 useCallback、useMemo 等鈎子,其實他與原來 Class 組件時用的 React.PureComponent、shouldComponentUpdate、React.memo 都是密切相關的。

本篇我們就從 useCallback 的使用角度入手,看看到底我們套了這一層鈎子能有什么作用,到底什么時候要套這一層鈎子?

正文

0.環境准備

首先我們先准備一下實驗環境,我們需要幾個鈎子和一個用於檢查的方法

首先是兩個版本的 useInput 的鈎子, 負責對應於一個 <input> 標簽

/**
 * 基本用法
 * @returns
 */
const useInput = () => {
  const [ value, setValue ] = useState('')
  const handleChange = (e) => setValue(e.target.value)
  return [ value, handleChange ]
}
/**
 * 帶 useCallback 用法
 * @returns
 */
const useInputWithUseCallback = () => {
  const [ value, setValue ] = useState('')
  const handleChange = useCallback(
    (e) => setValue(e.target.value),
    []
  )
  return [ value, handleChange ]
}

接下來我們需要一個特別的函數,來檢查組件的更新以及內部函數的替換與否

const funcsMap = new Map();

export const rec = (tag, fn) => {
  if (!funcsMap.has(tag)) {
    funcsMap.set(tag, []);
  }
  const funcs = funcsMap.get(tag);
  if (funcs.includes(fn)) {
    console.log(`[${tag}] fn exists`);
  } else {
    funcs.push(fn);
    console.log(`[${tag}] funcs:`, funcs);
  }
};

1.普通場景

第一個場景非常簡單,就是啥也不用,只用了最基本的 useState 鈎子來完成 input 事件

// test1 基礎代碼
const Test1 = () => {
  console.log("-- test1 render --");
  const [name, handleNameChange] = useInput();
  rec("Test1", handleNameChange);
  return (
    <div>
      <h2>Test 1: 測試基本情況(簡單 inline 函數)</h2>
      <h3>name: {name}</h3>
      <input type="text" value={name} onChange={handleNameChange} />
    </div>
  );
};

效果:

image

看起來沒啥不對勁的,輸入一次就要更新一次組件,所以就會調用一次方法,挺正常的。

2.存在子組件

第二個場景比較特別,現在我們在組件內部插入一個子組件


const Inner = (props) => {
  console.log(' -- render Test2 inner--')
  return (
    <input type="text" onChange={props.onChange} />
  )
}

// test2 基本情況 & 子組件
const Test2 = () => {
  console.log("-- test2 render --");
  const [name, handleNameChange] = useInput();
  rec("Test2", handleNameChange);
  return (
    <div>
      <h2>Test 2: 基本情況 & 子組件</h2>
      <h3>name: {name}</h3>
      <Inner onChange={handleNameChange} />
    </div>
  );
};

效果:

image

看起來也沒啥大問題。React 本身就是一個單向數據流,所以 Test2 更新 Inner 也跟着更新也是理所當然。

3.沒有子組件的情況下使用 useCallback 的 useInput

// 沒有子組件的情況下使用 useCallback
const Test3 = () => {
  console.log("-- test3 render --");
  const [name, handleNameChange] = useInputWithUseCallback();
  rec("Test3", handleNameChange);
  return (
    <div>
      <h2>Test 3: 使用 useCallback</h2>
      <h3>name: {name}</h3>
      <input type="text" value={name} onChange={handleNameChange} />
    </div>
  );
};

效果:

iamge

看起來也跟第一次實驗沒什么區別,但是細看輸出我們會發現,原本實驗每次更新之后給到 rec 函數的都是新的函數,而這次用了 useCallaback 之后,就變成都是同一個了, useCallback 就好像記憶了上次更新的那個函數,挪到下次更新的時候進行服用

下面我們看看這個概念到底能起到什么作用

4.存在子組件的情況下使用 useCallback

既然上面看到 useCallback 能幫我們記住前一次組件更新時的函數,那是不是再加上子組件他就不會更新了呢?

const Inner = (props) => {
  console.log('render Test4 Inner');
  return <input type="text" onChange={props.onChange} />;
};

const Test4 = () => {
  console.group('render Test4');

  const [name, handleNameChange] = useInputWithUseCallback();

  rec('Test4', handleNameChange);

  console.groupEnd();
  return (
    <div>
      <h2>Test 4: useCallback 與子組件</h2>
      <h3>name: {name}</h3>
      <Inner onChange={handleNameChange} />
    </div>
  );
};

image

結果事與願違,子組件還是非常勤奮的每次都更新一次

React 組件概念 & 組件更新機制梳理

  • 組件與實例方法

這邊就要提到 React 組件的更新機制了。 我們知道 React 組件分成使用 Class 聲明的類組件,以及使用 function 聲明的函數組件。

對於類組件來說,所有方法都是綁定在一個叫做組件實例的對象上,所以每次組件更新的時候都是傳入同一個函數實例;而對於函數組件來說就需要使用 useCallback 方法來確保傳入同一個函數實例

  • 組件的更新

不論是哪種組件的更新時機都是 props 與 state 的改變。默認情況下只要重新傳遞 props 或是 state 改變,組件就會直接更新。如果使用 React.pureComponent 則會對 props 進行淺比較,選擇性的更新;更新一步我們還可以使用類組件的 shouldComponentUpdate 鈎子來自定義組件更新的時機。

而對於函數組件來說,組件本身就是一個 render 方法,也沒有什么鈎子方法,這時候我們就可以使用 React.memo 來實現類似的功能

5.使用 React.memo 與 useCallabck 協同

了解上述更新機制之后,我們就能夠借由 useCallback 來保留相同的函數,在傳入 React.memo 包含的組件時實現子組件直接復用而不更新的功能了

const Inner = React.memo((props) => {
  console.log("render Test5 Inner");
  return <input type="text" onChange={props.onChange} />;
});

const Test5 = () => {
  console.group("render Test5");

  const [name, handleNameChange] = useInputWithUseCallback();

  rec("Test5", handleNameChange);

  console.groupEnd();
  return (
    <div>
      <h2>Test 5: useCallback 與 React.memo 協同</h2>
      <h3>name: {name}</h3>
      <Inner onChange={handleNameChange} />
    </div>
  );
};

效果:

image

6.使用 useMemo 與 useCallback 協同

最后一個場景也很簡單,就是使用 useMemo 來代替 React.memo 的用法

const Inner6 = (props) => {
  console.log("render Test6 Inner");
  return <input type="text" onChange={props.onChange} />;
};

const Test6 = () => {
  console.group("-- test6 render --");

  const [name, handleNameChange] = useInputWithUseCallback();

  rec("Test6", handleNameChange);

  console.groupEnd();
  return (
    <div>
      <h2>Test 6: useMemo 實現局部刷新</h2>
      <h3>name: {name}</h3>
      {
        useMemo(() => <Inner6 onChange={handleNameChange} />, [handleNameChange])
      }
    </div>
  );
};

效果:

image

useCallback 與 useMemo 的好處

由上述例子我們大概能夠知道兩個鈎子的用法,下面說說這兩個鈎子的意義

  • useCallback

    useCallback 本身可以看作類組件實例方法的替代方案,
    同時他又給我們開啟一個視角:在調用前就能夠綁定好變量(猶如閉包一般),並且在依賴數據不改變的情況下保留相同的函數實例

  • useMemo

    useMemo可以分為兩個角度來說明
    一方面可以將其看成就是 React.memo 的替代品,同時可以對任意大小的 JSX 片段進行緩存
    另一方面其實它讓我們能夠對組件內部的各個元素進行更細粒度的控制,讓我們能夠不只是利用 React.memo 粗暴的對整個組件進行記憶,而可以針對特定片段進行緩存與復用

7.總結 & 使用時機

最后我們總結呼應一下標題:

useCallback 的作用在於保留相同的函數,其本身並不干涉組件更新的機制;與 react.memo 或是 useMemo 等方法聯用時,才會獲得保留相同組件示例的傳入使局部片段能夠進行復用而不會完全更新的優化好處

結語

本文章透過針對 useCallback 的用法和各個場景進行探討, 嘗試歸納出 useCallback 的效果和使用時機,供大家學習思考

其他資源鏈接

useCallback - React官方文檔
useMemo - React官方文檔
React Hooks 第一期:聊聊 useCallback
TypeScript and React: Events
React.memo 與 useMemo
React.Component - React官方文檔
React 優化: 到底怎么用 useCallback 才是正確的?

完整代碼示例

codesandbox源代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM