關注 小賊先生,查看更多前端文章
Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。
useCallback
和useMemo
是其中的兩個 hooks,本文旨在經過解決一個需求,結合高階函數,深刻理解useCallback
和useMemo
的用法和使用場景。
之因此會把這兩個 hooks 放到一塊兒說,是由於他們的主要做用都是性能優化,且使用useMemo
能夠實現useCallback
。html
先把需求拎出來講下,而後順着需求往下捋useCallback
和useMemo
,這樣更好理解爲何要使用這兩個 hooks。前端
需求是:當鼠標在某個 dom 標籤上移動的時候,記錄鼠標的普通移動次數和加了防抖處理後的移動次數。[如圖]:react
useCallback
和useMemo
,因此遇到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
內的setTimeout
。setBounceCount
是在setTimeout
內執行的,也就是異步的,等待時間約是1000ms,而handleMouseMove
雖然是事件回調函數,但鼠標移動時,這個函數的執行間隔相比1000ms要短不少,最終使得bounceCount
參數值老是相同的,因此整個效果纔像通過了防抖處理同樣。異步
咱們使用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
緩存了下來,解決了debounceSetCount
在Example
每次渲染時老是指向不一樣debounced
的問題。
咱們上面說過,這裏面其實還有一個問題,那就是每次Example
組件更新的時候,debounce
函數都會執行一次,經過上面的分析咱們知道,這是一次無用的執行,若是此處的debounce
函數裏有大量的計算的話,就會很影響性能。
看下使用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
,你有什麼好的解決方案呢,歡迎留言討論。
關注 小賊先生,查看更多前端文章