memo、useMemo及useCallback解析

前言

在hooks誕生以前,若是組件包含內部 state,咱們都是基於 class 的形式來建立組件。css

在react中,性能優化的點在於:react

  1. 調用 setState,就會觸發組件的從新渲染,不管先後 state 是否相同
  2. 父組件更新,子組件也會自動更新

基於上面的兩點,咱們一般的解決方案是:使用 immutable 進行比較,在不相等的時候調用 setState, 在 shouldComponentUpdate 中判斷先後的 propsstate,若是沒有變化,則返回 false 來阻止更新。ios

hooks 出來以後,函數組件中沒有 shouldComponentUpdate 生命週期,咱們沒法經過判斷先後狀態來決定是否更新。useEffect 再也不區分 mount update 兩個狀態,這意味着函數組件的每一次調用都會執行其內部的全部邏輯,那麼會帶來較大的性能損耗。c++

對比

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

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

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

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

React.memo()

在 class 組件時代,爲了性能優化咱們常常會選擇使用 PureComponent,每次對 props 進行一次淺比較,固然,除了 PureComponent 外,咱們還能夠在 shouldComponentUpdate 中進行更深層次的控制。緩存

在 Function 組件中, React 貼心的提供了 React.memo 這個 HOC(高階組件),與 PureComponent 很類似,可是是專門給 Function Component 提供的,對 Class Component 並不適用。性能優化

可是相比於 PureComponent ,React.memo() 能夠支持指定一個參數,能夠至關於 shouldComponentUpdate 的做用,所以 React.memo() 相對於 PureComponent 來講,用法更加方便。網絡

(固然,若是本身封裝一個 HOC,而且內部實現 PureComponent + shouldComponentUpdate 的結合使用,確定也是 OK 的,在以往項目中,這樣使用的方式還挺多)

首先看下 React.memo() 的使用方式:

function MyComponent(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  /* return true if passing nextProps to render would return the same result as passing prevProps to render, otherwise return false */
}
export default React.memo(MyComponent, areEqual);
複製代碼

使用方式很簡單,在 Function Component 以外,在聲明一個 areEqual 方法來判斷兩次 props 有什麼不一樣,若是第二個參數不傳遞,則默認只會進行 props 的淺比較

最終 export 的組件,就是 React.memo() 包裝以後的組件。

實例:

  • index.js:父組件

  • Child.js:子組件

  • ChildMemo.js:使用 React.memo 包裝過的子組件

index.js

import React, { useState, } from 'react';
import Child from './Child';
import ChildMemo from './Child-memo';

export default (props = {}) => {
    const [step, setStep] = useState(0);
    const [count, setCount] = useState(0);
    const [number, setNumber] = useState(0);

    const handleSetStep = () => {
        setStep(step + 1);
    }

    const handleSetCount = () => {
        setCount(count + 1);
    }

    const handleCalNumber = () => {
        setNumber(count + step);
    }

    return (
        <div>
            <button onClick={handleSetStep}>step is : {step} </button>
            <button onClick={handleSetCount}>count is : {count} </button>
            <button onClick={handleCalNumber}>numberis : {number} </button>
            <hr />
            <Child step={step} count={count} number={number} /> <hr />
            <ChildMemo step={step} count={count} number={number} />
        </div>
    );
}
複製代碼

child.js

這個子組件自己沒有任何邏輯,也沒有任何包裝,就是渲染了父組件傳遞過來的 props.number

須要注意的是,子組件中並無使用到 props.stepprops.count,可是一旦 props.step 發生了變化就會觸發從新渲染。

import React from 'react';

export default (props = {}) => {
    console.log(`--- re-render ---`);
    return (
        <div> {/* <p>step is : {props.step}</p> */} {/* <p>count is : {props.count}</p> */} <p>number is : {props.number}</p> </div>
    );
};
複製代碼

childMemo.js

這個子組件使用了 React.memo 進行了包裝,而且經過 isEqual 方法判斷只有當兩次 props 的 number 的時候纔會從新觸發渲染,不然 console.log 也不會執行。

import React, { memo, } from 'react';

const isEqual = (prevProps, nextProps) => {
    if (prevProps.number !== nextProps.number) {
        return false;
    }
    return true;
}

export default memo((props = {}) => {
    console.log(`--- memo re-render ---`);
    return (
        <div> {/* <p>step is : {props.step}</p> */} {/* <p>count is : {props.count}</p> */} <p>number is : {props.number}</p> </div>
    );
}, isEqual);
複製代碼

效果對比

avatar

經過上圖能夠發現,在點擊 step 和 count 的時候,props.step 和 props.count 都發生了變化,所以 Child.js 這個子組件每次都在從新執行渲染(----re-render----),即便沒有用到這兩個 props。

而這種狀況下,ChildMemo.js 則不會從新進行 re-render。

只有當 props.number 發生變化的時候,ChildMemo.jsChild.js 表現是一致的。

從上面能夠看出,React.memo() 的第二個方法在某種特定需求下,是必須存在的。 由於在實驗的場景中,咱們可以看得出來,即便我使用 React.memo 包裝了 Child.js,也會一直觸發從新渲染,由於 props 淺比較確定是發生了變化。

React.useMemo() 細粒度性能優化

上面 React.memo() 的使用咱們能夠發現,最終都是在最外層包裝了整個組件,而且須要手動寫一個方法比較那些具體的 props 不相同才進行 re-render。

而在某些場景下,咱們只是但願 component 的部分不要進行 re-render,而不是整個 component 不要 re-render,也就是要實現 局部 Pure 功能。

useMemo() 基本用法以下:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼

useMemo() 返回的是一個 memoized 值,只有當依賴項(好比上面的 a,b 發生變化的時候,纔會從新計算這個 memoized 值)

memoized 值不變的狀況下,不會從新觸發渲染邏輯。

提及渲染邏輯,須要記住的是 useMemo() 是在 render 期間執行的,因此不能進行一些額外的副操做,好比網絡請求等。

若是沒有提供依賴數組(上面的 [a,b])則每次都會從新計算 memoized 值,也就會 re-redner

上面的代碼中新增一個 Child-useMemo.js 子組件以下:

import React, { useMemo } from 'react';

export default (props = {}) => {
    console.log(`--- component re-render ---`);
    return useMemo(() => {
        console.log(`--- useMemo re-render ---`);
        return <div> {/* <p>step is : {props.step}</p> */} {/* <p>count is : {props.count}</p> */} <p>number is : {props.number}</p> </div>
    }, [props.number]);
}
複製代碼

與上面惟一的區別是使用的 useMemo() 包裝的是 return 部分渲染的邏輯,而且聲明依賴了 props.number,其餘的並未發生變化。

效果對比:

avatar

上面圖中咱們能夠發現,父組件每次更新 step/count 都會觸發 useMemo 封裝的子組件的 re-render,可是 number 沒有變化,說明並無從新觸發 HTML 部分 re-render

只有當依賴的 props.number 發生變化的時候,纔會從新觸發 useMemo() 包裝的裏面的 re-render

React.useCallback()

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

const fnA = useCallback(fnB, [a])
複製代碼

實例:

import React, { useState, useCallback } from 'react';
import Button from './Button';

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleClickButton1 = () => {
    setCount1(count1 + 1);
  };

  const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]);

  return (
    <div> <div> <Button onClickButton={handleClickButton1}>Button1</Button> </div> <div> <Button onClickButton={handleClickButton2}>Button2</Button> </div> </div>
  );
}
複製代碼

Button組件

// Button.jsx
import React from 'react';

const Button = ({ onClickButton, children }) => {
  return (
    <> <button onClick={onClickButton}>{children}</button> <span>{Math.random()}</span> </> ); }; export default React.memo(Button); 複製代碼

這裏或許會注意到 React.memo 這個方法,此方法內會對 props 作一個淺層比較,若是若是 props 沒有發生改變,則不會從新渲染此組件。

上面的 Button 組件都須要一個 onClickButton 的 props ,儘管組件內部有用 React.memo 來作優化,可是咱們聲明的 handleClickButton1 是直接定義了一個方法,這也就致使只要是父組件從新渲染(狀態或者props更新)就會致使這裏聲明出一個新的方法,新的方法和舊的方法儘管長的同樣,可是依舊是兩個不一樣的對象,React.memo 對比後發現對象 props 改變,就從新渲染了。

const handleClickButton2 = useCallback(() => {
  setCount2(count2 + 1);
}, [count2]);
複製代碼

上述代碼咱們的方法使用 useCallback 包裝了一層,而且後面還傳入了一個 [count2] 變量,這裏 useCallback 就會根據 count2 是否發生變化,從而決定是否返回一個新的函數,函數內部做用域也隨之更新。

因爲咱們的這個方法只依賴了 count2 這個變量,並且 count2 只在點擊 Button2 後纔會更新 handleClickButton2,因此就致使了咱們點擊 Button1 不從新渲染 Button2 的內容。

總結

  1. 在子組件不須要父組件的值和函數的狀況下,只須要使用 memo 函數包裹子組件便可。

  2. 若是有函數傳遞給子組件,使用 useCallback

  3. 若是有值傳遞給子組件,使用 useMemo

  4. useEffectuseMemouseCallback 都是自帶閉包的。也就是說,每一次組件的渲染,其都會捕獲當前組件函數上下文中的狀態(state, props),因此每一次這三種hooks的執行,反映的也都是當前的狀態,你沒法使用它們來捕獲上一次的狀態。對於這種狀況,咱們應該使用 ref 來訪問。

相關文章
相關標籤/搜索