在實踐開發中,有一種優化手段叫做記憶函數。
什麼是記憶函數?用一個例子來說明。
我們想要計算從1到某個整數的總和。封裝一個方法來實現這個目的。
function summation(target: number) { let sum = 0; for(let i = 1; i <= target; i++) { sum += i; } return sum;}驗證一下結果,沒有問題。
這個時候,我們思考一個問題,當我們重複調用summation(100)時,函數內部的循環計算是不是有點冗餘?因為傳入的參數一樣,得到的結果必定也是一樣,因此如果傳入的參數一致,是不是可以不用再重複計算直接用上次的計算結果返回呢?
當然可以,利用閉包能夠實現我們的目的。
let preTarget = -1;let memoSum = 0;
export function memoSummation(target: number) { if (preTarget > 0 && preTarget === target) { return memoSum; }
console.log('我出現,就表示重新計算了一次'); preTarget = target; let sum = 0; for (let i = 1; i <= target; i++) { sum += i; } memoSum = sum; return sum;}多次調用memoSummation(1000),沒有問題,我們的目的達到了。後兩次的調用直接返回了記憶中的結果。
這就是記憶函數。記憶函數利用閉包,在確保返回結果一定正確的情況下,減少了重複冗餘的計算過程。這是我們試圖利用記憶函數去優化我們代碼的目的所在。
1react hooks提供的api,大多都有記憶功能。例如
•useState•useEffect•useLayoutEffect•useReducer•useRef•useMemo 記憶計算結果•useCallback 記憶函數體
其他幾個api的使用方法,我們在前面已經一一跟大家分析過。這裡主要關注useMemo與useCallback。
useMemo
useMemo緩存計算結果。它接收兩個參數,第一個參數為計算過程(回調函數,必須返回一個結果),第二個參數是依賴項(數組),當依賴項中某一個發生變化,結果將會重新計算。
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;useCallback
useCallback的使用幾乎與useMemo一樣,不過useCallback緩存的是一個函數體,當依賴項中的一項發現變化,函數體會重新創建。
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;寫一個案例,來觀察一下他們的使用。
import React, { useMemo, useState, useCallback } from 'react';import { Button } from 'antd-mobile';
export default function App() { const [target, setTarget] = useState(0); const [other, setOther] = useState(0)
const sum = useMemo(() => { console.log('重新計算一次'); let _sum = 0; for (let i = 1; i <= target; i++) { _sum += i; } return _sum; }, [target]);
const inputChange = useCallback((e) => { console.log(e.target.value); }, []);
return ( <div style={{ width: '200px', margin: 'auto' }}> <input type="text" onChange={inputChange} /> <div style={{ width: '80px', margin: '100px auto', fontSize: '40px' }}>{target} {sum}</div> <Button onClick={() => setTarget(target + 1)}>遞增</Button> <Button onClick={() => setTarget(target - 1)}>遞減</Button>
<div style={{ width: '80px', margin: '100px auto', fontSize: '20px' }}>幹擾項 {other}</div> <Button onClick={() => setOther(other + 1)}>遞增</Button> <Button onClick={() => setOther(other - 1)}>遞減</Button> </div> )}2useMemo/useCallback的使用非常簡單,不過我們需要思考一個問題,使用他們一定能夠達到優化的目的嗎?
React的學習經常容易陷入過度優化的誤區。一些人在得知shouldComponentUpdate能夠優化性能,恨不得每個組件都要用一下,不用就感覺自己的組件有問題。useMemo/useCallback也是一樣。
明白了記憶函數的原理,我們應該知道,記憶函數並非完全沒有代價,我們需要創建閉包,佔用更多的內存,用以解決計算上的冗餘。
useMemo/useCallback也是一樣,這是一種成本上的交換。那麼我們在使用時,就必須要思考,這樣的交換,到底值不值?
如果不使用useCallback,我們就必須在函數組件內部創建超多的函數,這種情況是不是就一定有性能問題呢?
不是的。
我們知道,一個函數執行完畢之後,就會從函數調用棧中被彈出,裡面的內存也會被回收。因此,即使在函數內部創建了多個函數,執行完畢之後,這些創建的函數也都會被釋放掉。函數式組件的性能是非常快的。相比class,函數更輕量,也避免了使用高階組件、renderProps等會造成額外層級的技術。使用合理的情況下,性能幾乎不會有什麼問題。
而當我們使用useMemo/useCallback時,由於新增了對於閉包的使用,新增了對於依賴項的比較邏輯,因此,盲目使用它們,甚至可能會讓你的組件變得更慢。
大多數情況下,這樣的交換,並不划算,或者賺得不多。你的組件可能並不需要使用useMemo/useCallback來優化。
3那麼,什麼時候使用useMemo/useCallback比較合適?
總的原則,就是當你認為,交換能夠賺的時候去使用它們。
例如在一個一定會多次re-render的組件裡,input的回調沒有任何依賴項,我們就可以使用useCallback來降低多次執行帶來的重複創建同樣方法的負擔。
即使這樣,也可能並不會優化多少,因為我們緩存的函數體本身就非常簡單,不會造成太大的負擔
<input type="text" onChange={inputChange} />
const inputChange = useCallback((e) => { setValue(e.target.value);}, []);但是,同樣的場景,如果該組件一定只會渲染一次,那麼使用useCallback就完全沒有必要。
通常情況下,當函數體或者結果的計算過程非常複雜時,我們才會考慮優先使用useCallback/useMemo。
例如,在日曆組件中,需要根據今天的日期,計算出當月的所有天數以及相關的信息。
不過,當依賴項會頻繁變動時,我們也要考慮使用useMemo/useCallback是否划算。
每當依賴項變動,useMemo/useCallback不會直接返回計算結果,這個時候,結果會重新計算,函數體會重新創建。因此依賴項變動頻繁時,需要慎重考慮。
最後,一圖總結全文。
本系列文章的所有案例,都可以在下面的地址中查看
https://github.com/advance-course/react-hooks
本系列文章為原創,請勿私自轉載,轉載請務必私信我
關於如何學好JavaScript,我寫了一本書,感興趣的同學可點擊閱讀原文查看詳情。