換個角度,結合高階函數聊聊React的useMemo和useCallback

關注 小賊先生,查看更多前端文章
Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。

useCallbackuseMemo是其中的兩個 hooks,本文旨在經過解決一個需求,結合高階函數,深刻理解useCallbackuseMemo的用法和使用場景。
之因此會把這兩個 hooks 放到一塊兒說,是由於他們的主要做用都是性能優化,且使用useMemo能夠實現useCallbackhtml

需求說明

先把需求拎出來講下,而後順着需求往下捋useCallbackuseMemo,這樣更好理解爲何要使用這兩個 hooks。前端

需求是:當鼠標在某個 dom 標籤上移動的時候,記錄鼠標的普通移動次數和加了防抖處理後的移動次數。[如圖]:
react

技術儲備

  • 本文主要介紹useCallbackuseMemo,因此遇到useState時就不作特殊說明了,若是對useState還不瞭解,請參看官方文檔
  • 該需求須要用到防抖函數,爲方便調試,先準備一個簡單的防抖函數(一個高階函數):
function debounce(func, delay = 1000) {
  let timer;

  function debounced(...args) {
    debounced.cancel();
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  }

  debounced.cancel = function () {
    if (timer !== undefined) {
      clearTimeout(timer);
      timer = undefined;
    }
  }
  return debounced
}

不合格的解決方案

根據需求,寫出來組件大體會是這樣:數組

function Example() {
  const [count, setCount] = useState(0);
  const [bounceCount, setBounceCount] = useState(0);
  const debounceSetCount = debounce(setBounceCount);

  const handleMouseMove = () => {
    setCount(count + 1);
    debounceSetCount(bounceCount + 1);
  };

  return (
    <div onMouseMove={handleMouseMove}>
      <p>普通移動次數: {count}</p>
      <p>防抖處理後移動次數: {bounceCount}</p>
    </div>
  )
}

效果貌似是對的,在debounced裏打印日誌看下:緩存

function debounce(func, delay = 1000) {
    // ... 省略其餘代碼
    timer = setTimeout(() => {
      // 在此處添加了一行打印代碼
      console.log('run-do');
      func.apply(this, args);
    }, delay);
    // ... 省略其餘代碼
}

當鼠標在div標籤上移動時,打印結果[如圖]:性能優化

咱們發現,當鼠標中止移動後,run-do被打印的次數,跟鼠標移動次數相同,這說明防抖功能並未生效。是哪裏出問題了呢?閉包

首先咱們要清楚的是,使用debounce的目的是經過debounce返回一個debounced函數(注意:此處是debounced,而不是debounce,下文一樣要注意這個細節,不然意思就徹底不對了),而後每次執行debounced時,經過閉包內的timer清掉以前的setTimeout,達到一段時間不活動後執行任務的目的。app

再來看看咱們的Example組件,每次Example組件的更新渲染,都會經過debounce(setBounceCount)生成一個新的debounceSetCount,也就是每次的更新渲染,debounceSetCount都是指向不一樣的debounced,不一樣的debounced使用着不一樣的timer,那麼debounce函數裏的閉包就失去了意義,因此纔會出現截圖中的狀況。dom

可是,爲何bounceCount的值看着像是進行過防抖處理同樣呢?
由於debounceSetCount(bounceCount + 1)在屢次執行時,debounce內的setTimeoutsetBounceCount是在setTimeout內執行的,也就是異步的,等待時間約是1000ms,而handleMouseMove雖然是事件回調函數,但鼠標移動時,這個函數的執行間隔相比1000ms要短不少,最終使得bounceCount參數值老是相同的,因此整個效果纔像通過了防抖處理同樣。異步

useCallback

咱們使用useCallback修改下咱們的組件:

function Example() {
  // ... 省略其餘代碼
  // 相比以前的 Example 組件,咱們只是增長了 useCallback hook
  const debounceSetCount = React.useCallback(debounce(setBounceCount), []);
  // ... 省略其餘代碼
}

這時再用鼠標在div標籤上移動時,效果跟咱們的需求一致了,[如圖]:

經過useCallback,咱們貌似解決了以前存在的問題(其實這裏面還有問題,咱們後面會說到)。

那麼,useCallback是怎麼解決問題的呢?
看下useCallback的調用簽名:

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

// 示例:
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

經過useCallback的簽名能夠知道,useCallback第一個參數是一個函數,返回一個 memoized 回調函數,如上面代碼中的 memoizedCallback 。useCallback的第二個參數是依賴(deps),當依賴改變時才更新 memoizedCallback ,也就是在依賴未改變時(或空數組無依賴時), memoizedCallback 老是指向同一個函數,也就是指向同一塊內存區域。當把 memoizedCallbac 看成 props 傳遞給子組件時,子組件就能夠經過shouldComponentUpdate等手段避免沒必要要的更新。

Example組件首次渲染時,debounceSetCount的值是debounce(setBounceCount)的執行結果,由於經過useCallback生成debounceSetCount時,傳入的依賴是空數組,因此Example組件在下一次渲染時,debounceSetCount會忽略debounce(setBounceCount)的執行結果,老是返回Example第一次渲染時useCallback緩存的結果,也就是說debounce(setBounceCount)的執行結果經過useCallback緩存了下來,解決了debounceSetCountExample每次渲染時老是指向不一樣debounced的問題。

咱們上面說過,這裏面其實還有一個問題,那就是每次Example組件更新的時候,debounce函數都會執行一次,經過上面的分析咱們知道,這是一次無用的執行,若是此處的debounce函數裏有大量的計算的話,就會很影響性能。

useMemo

看下使用useMemo如何解決這個問題呢:

function Example() {
  const [count, setCount] = useState(0);
  const [bounceCount, setBounceCount] = useState(0);
  const debounceSetCount = React.useMemo(() => debounce(setBounceCount), []);

  const handleMouseMove = () => {
    setCount(count + 1);
    debounceSetCount(bounceCount + 1);
  };

  return (
    <div onMouseMove={handleMouseMove} >
      <p>普通移動次數: {count}</p>
      <p>防抖處理後移動次數: {bounceCount}</p>
    </div>
  )
}

如今,每次Example更新渲染時,debounceSetCount都是指向同一塊內存,並且debounce只會執行一次,咱們的需求完成了,咱們的問題也都獲得瞭解決。

小賊先生-文章原址

useMemo是怎麼作到的呢?
看下useMemo的調用簽名:

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

// 示例:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

經過useMemo的簽名能夠知道,useMemo第一個參數是一個 factory 函數,該函數的返回結果會經過useMemo緩存下來,只有當useMemo的依賴(deps)改變時才從新執行 factory 函數,memoizedValue 纔會被從新計算。 也就是在依賴未改變時(或空數組無依賴時),memoizedValue 老是返回經過useMemo緩存的值。

看到這裏,相信細心的你也已經發現了,useCallback(fn, deps) 其實至關於 useMemo(() => fn, deps),因此在最開始咱們說:使用useMemo徹底能夠實現useCallback

特別注意

React 官方有這麼一句話:
你能夠把 useMemo 做爲性能優化的手段,但不要把它當成語義上的保證。未來,React 可能會選擇「遺忘」之前的一些 memoized 值,並在下次渲染時從新計算它們,好比爲離屏組件釋放內存。先編寫在沒有 useMemo 的狀況下也能夠執行的代碼 —— 以後再在你的代碼中添加 useMemo,以達到優化性能的目的。 查看原文

顯然,咱們的代碼中,若是去掉useMemo是會出問題的,對此,可能有人會想,改裝下debounce防抖函數就能夠了,例如:

function debounce(func, ...args) {
  if (func.timeId !== undefined) {
    clearTimeout(func.timeId);
    func.timeId = undefined;
  }

  func.timeId = setTimeout(() => {
    func(...args);
  }, 200);
}

// 使用 useCallback
function Example() {
  // ... 省略其餘代碼
  const debounceSetCount = React.useCallback((...args) => {
    debounce(setBounceCount, ...args);
  }, []);
  // ... 省略其餘代碼
}

// 不使用 useCallback
function Example() {
  // ... 省略其餘代碼
  const debounceSetCount = changeCount => debounce(setBounceCount, changeCount);
  // ... 省略其餘代碼
}

貌似去掉了useMemo也能實現咱們的需求,但顯然,這是一種很是將就的解決方案,一旦遇到像修改前的debounce這樣的高階函數就一籌莫展了。
那麼,若是不使用useMemo,你有什麼好的解決方案呢,歡迎留言討論。

關注 小賊先生,查看更多前端文章

參考文檔

相關文章
相關標籤/搜索