useMemo與useCallback使用指南


上一篇文章介紹了useEffect的使用,接下來准備介紹useCallback和useMemo。

回顧

在介紹一下這兩個hooks的作用之前,我們先來回顧一下react中的性能優化。在hooks誕生之前,如果組件包含內部state,我們都是基於class的形式來創建組件。當時我們也知道,react中,性能的優化點在於:

  1. 調用setState,就會觸發組件的重新渲染,無論前后的state是否不同
  2. 父組件更新,子組件也會自動的更新

基於上面的兩點,我們通常的解決方案是:使用immutable進行比較,在不相等的時候調用setState;在shouldComponentUpdate中判斷前后的props和state,如果沒有變化,則返回false來阻止更新。

在hooks出來之后,我們能夠使用function的形式來創建包含內部state的組件。但是,使用function的形式,失去了上面的shouldComponentUpdate,我們無法通過判斷前后狀態來決定是否更新。而且,在函數組件中,react不再區分mount和update兩個狀態,這意味着函數組件的每一次調用都會執行其內部的所有邏輯,那么會帶來較大的性能損耗。因此useMemo 和useCallback就是解決性能問題的殺手鐧。

對比

我們先簡單的看一下useMemo和useCallback的調用簽名:

function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T; function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

useCallback和useMemo的參數跟useEffect一致,他們之間最大的區別有是useEffect會用於處理副作用,而前兩個hooks不能。

useMemo和useCallback都會在組件第一次渲染的時候執行,之后會在其依賴的變量發生改變時再次執行;並且這兩個hooks都返回緩存的值,useMemo返回緩存的變量,useCallback返回緩存的函數。

useMemo

我們來看一個反例:

  1.  
    import React from 'react';
  2.  
     
  3.  
     
  4.  
    export default function WithoutMemo() {
  5.  
    const [count, setCount] = useState(1);
  6.  
    const [val, setValue] = useState('');
  7.  
     
  8.  
    function expensive() {
  9.  
    console.log('compute');
  10.  
    let sum = 0;
  11.  
    for (let i = 0; i < count * 100; i++) {
  12.  
    sum += i;
  13.  
    }
  14.  
    return sum;
  15.  
    }
  16.  
     
  17.  
    return <div>
  18.  
    <h4>{count}-{val}-{expensive()}</h4>
  19.  
    <div>
  20.  
    <button onClick={() => setCount(count + 1)}>+c1</button>
  21.  
    <input value={val} onChange={event => setValue(event.target.value)}/>
  22.  
    </div>
  23.  
    </div>;
  24.  
    }

這里創建了兩個state,然后通過expensive函數,執行一次昂貴的計算,拿到count對應的某個值。我們可以看到:無論是修改count還是val,由於組件的重新渲染,都會觸發expensive的執行(能夠在控制台看到,即使修改val,也會打印);但是這里的昂貴計算只依賴於count的值,在val修改的時候,是沒有必要再次計算的。在這種情況下,我們就可以使用useMemo,只在count的值修改時,執行expensive計算:

  1.  
    export default function WithMemo() {
  2.  
    const [count, setCount] = useState(1);
  3.  
    const [val, setValue] = useState('');
  4.  
    const expensive = useMemo(() => {
  5.  
    console.log('compute');
  6.  
    let sum = 0;
  7.  
    for (let i = 0; i < count * 100; i++) {
  8.  
    sum += i;
  9.  
    }
  10.  
    return sum;
  11.  
    }, [count]);
  12.  
     
  13.  
    return <div>
  14.  
    <h4>{count}-{expensive}</h4>
  15.  
    {val}
  16.  
    <div>
  17.  
    <button onClick={() => setCount(count + 1)}>+c1</button>
  18.  
    <input value={val} onChange={event => setValue(event.target.value)}/>
  19.  
    </div>
  20.  
    </div>;
  21.  
    }

上面我們可以看到,使用useMemo來執行昂貴的計算,然后將計算值返回,並且將count作為依賴值傳遞進去。這樣,就只會在count改變的時候觸發expensive執行,在修改val的時候,返回上一次緩存的值。

useCallback

講完了useMemo,接下來是useCallback。useCallback跟useMemo比較類似,但它返回的是緩存的函數。我們看一下最簡單的用法:

const fnA = useCallback(fnB, [a])

上面的useCallback會將我們傳遞給它的函數fnB返回,並且將這個結果緩存;當依賴a變更時,會返回新的函數。既然返回的是函數,我們無法很好的判斷返回的函數是否變更,所以我們可以借助ES6新增的數據類型Set來判斷,具體如下:

  1.  
    import React, { useState, useCallback } from 'react';
  2.  
     
  3.  
    const set = new Set();
  4.  
     
  5.  
    export default function Callback() {
  6.  
    const [count, setCount] = useState(1);
  7.  
    const [val, setVal] = useState('');
  8.  
     
  9.  
    const callback = useCallback(() => {
  10.  
    console.log(count);
  11.  
    }, [count]);
  12.  
    set.add(callback);
  13.  
     
  14.  
     
  15.  
    return <div>
  16.  
    <h4>{count}</h4>
  17.  
    <h4>{set.size}</h4>
  18.  
    <div>
  19.  
    <button onClick={() => setCount(count + 1)}>+</button>
  20.  
    <input value={val} onChange={event => setVal(event.target.value)}/>
  21.  
    </div>
  22.  
    </div>;
  23.  
    }

我們可以看到,每次修改count,set.size就會+1,這說明useCallback依賴變量count,count變更時會返回新的函數;而val變更時,set.size不會變,說明返回的是緩存的舊版本函數。

知道useCallback有什么樣的特點,那有什么作用呢?

使用場景是:有一個父組件,其中包含子組件,子組件接收一個函數作為props;通常而言,如果父組件更新了,子組件也會執行更新;但是大多數場景下,更新是沒有必要的,我們可以借助useCallback來返回函數,然后把這個函數作為props傳遞給子組件;這樣,子組件就能避免不必要的更新。

  1.  
    import React, { useState, useCallback, useEffect } from 'react';
  2.  
    function Parent() {
  3.  
    const [count, setCount] = useState(1);
  4.  
    const [val, setVal] = useState('');
  5.  
     
  6.  
    const callback = useCallback(() => {
  7.  
    return count;
  8.  
    }, [count]);
  9.  
    return <div>
  10.  
    <h4>{count}</h4>
  11.  
    <Child callback={callback}/>
  12.  
    <div>
  13.  
    <button onClick={() => setCount(count + 1)}>+</button>
  14.  
    <input value={val} onChange={event => setVal(event.target.value)}/>
  15.  
    </div>
  16.  
    </div>;
  17.  
    }
  18.  
     
  19.  
    function Child({ callback }) {
  20.  
    const [count, setCount] = useState(() => callback());
  21.  
    useEffect( () => {
  22.  
    setCount(callback());
  23.  
    }, [callback]);
  24.  
    return <div>
  25.  
    {count}
  26.  
    </div>
  27.  
    }

不僅是上面的例子,所有依賴本地狀態或props來創建函數,需要使用到緩存函數的地方,都是useCallback的應用場景。

多談一點

useEffect、useMemo、useCallback都是自帶閉包的。也就是說,每一次組件的渲染,其都會捕獲當前組件函數上下文中的狀態(state, props),所以每一次這三種hooks的執行,反映的也都是當前的狀態,你無法使用它們來捕獲上一次的狀態。對於這種情況,我們應該使用ref來訪問。

源代碼:

https://github.com/anymost/hooks-demo/blob/master/src/page/Memo.js​github.com

 

https://github.com/anymost/hooks-demo/blob/master/src/page/Callback.js​github.com

原文https://zhuanlan.zhihu.com/p/66166173


免責聲明!

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



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