useCallback/useMemo 的使用誤區

在編寫 React Hook 代碼時,useCallbackuseMemo時常使人感到困惑。儘管咱們知道他們的功能都是作緩存並優化性能,可是又會擔憂由於使用方法不正確致使負優化。本文將闡述useCallbackuseMemo在開發中常見的使用方式和誤區,並結合源碼剖析緣由,知其然並知其因此然。javascript

1.useCallback

1.1 不要濫用useCallback

考察以下示例:html

import React from 'react';

function Comp() {
    const onClick = () => {
        console.log('打印');
    }
    
    return <div onClick={onClick}>Comp組件</div>
}
複製代碼

Comp組件自身觸發刷新或做爲子組件跟隨父組件刷新時,咱們注意到onClick會被從新賦值。爲了"提高性能",使用useCallback包裹onClick以達到緩存的目的:java

import React, { useCallback } from 'react';

function Comp() {
    const onClick = useCallback(() => {
        console.log('打印');
    }, []);
    
    return <div onClick={onClick}>Comp組件</div>
}
複製代碼

那麼問題來了,性能到底有沒有得到提高?答案是非但沒有,反而不如之前了;咱們改寫代碼的邏輯結構以後,緣由就會很是清晰:react

import React, { useCallback } from 'react';

function Comp() {
    const onClick = () => {
        console.log('打印');
    };
    
    const memoOnClick = useCallback(onClick, []);
    
    return <div onClick={memoOnClick}>Comp組件</div>
}
複製代碼

每一行多餘代碼的執行都產生消耗,哪怕這消耗只是 CPU 的一丁點熱量。官方文檔指出,無需擔憂建立函數會致使性能問題,因此使用useCallback來改造該場景下的組件,咱們並未得到任何收益(函數仍是會被建立),反而其帶來的成本讓組件負重(須要對比依賴是否發生變化),useCallback用的越多,負重越多。站在 javascript 的角度,當組件刷新時,未被useCallback包裹的方法將被垃圾回收並從新定義,但被useCallback所製造的閉包將保持對回調函數和依賴項的引用。緩存

1.2 useCallback的正確使用方法

產生誤區的緣由是useCallback的設計初衷並不是解決組件內部函數屢次建立的問題,而是減小子組件的沒必要要重複渲染。實際上在 React 體系下,優化思路主要有兩種:markdown

  • 1.減小從新 render 的次數。由於 React 最耗費性能的就是調和過程(reconciliation),只要不 render 就不會觸發 reconciliation。
  • 2.減小計算量,這個天然沒必要多說。

因此考察以下場景:閉包

import React, { useState } from 'react';

function Comp() {
    const [dataA, setDataA] = useState(0);
    const [dataB, setDataB] = useState(0);

    const onClickA = () => {
        setDataA(o => o + 1);
    };
    
    const onClickB = () => {
        setDataB(o => o + 1);
    }
    
    return <div> <Cheap onClick={onClickA}>組件Cheap:{dataA}</div> <Expensive onClick={onClickB}>組件Expensive:{dataB}</Expensive> </div>
}
複製代碼

Expensive是一個渲染成本很是高的組件,但點擊Cheap組件也會致使Expensive從新渲染,即便dataB並未發生改變。緣由就是onClickB被從新定義,致使 React 在 diff 新舊組件時,斷定組件發生了變化。這時候useCabllbackmemo就發揮了做用:函數

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

function Expensive({ onClick, name }) {
  console.log('Expensive渲染');
  return <div onClick={onClick}>{name}</div>
}

const MemoExpensive = memo(Expensive);

function Cheap({ onClick, name }) {
  console.log('cheap渲染');
  return <div onClick={onClick}>{name}</div>
}

export default function Comp() {
    const [dataA, setDataA] = useState(0);
    const [dataB, setDataB] = useState(0);

    const onClickA = () => {
        setDataA(o => o + 1);
    };
    
    const onClickB = useCallback(() => {
        setDataB(o => o + 1);
    }, []);
    
    return <div>
        <Cheap onClick={onClickA} name={`組件Cheap:${dataA}`}/>
        <MemoExpensive onClick={onClickB} name={`組件Expensive:${dataB}`} />
    </div>
}
複製代碼

memo是 React v16.6.0 新增的方法,與 PureComponent 相似,前者負責 Function Component 的優化,後者負責 Class Component。它們都會對傳入組件的新舊數據進行淺比較,若是相同則不會觸發渲染。工具

因此useCallback保證了onClickB不發生變化,此時點擊Cheap組件不會觸發Expensive組件的刷新,只有點擊Expensive組件纔會觸發。在實現減小沒必要要渲染的優化過程當中,useCallbackmemo是一對利器。運行示例代碼oop

1.3 延伸

useCallback源碼以下:

// 初始化階段
function mountCallback(callback, deps) {
    const hook = mountWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    hook.memoizedState = [callback, nextDeps];
    return callback;
}

// 更新階段
function updateCallback(callback, deps) {
    const hook = updateWorkInProgressHook();,
    const nextDeps = deps === undefined ? null : deps;
    const prevState = hook.memoizedState;
    if (prevState !== null) {
        if (nextDeps !== null) {
            const prevDeps = prevState[1];
            // 比較是否相等
            if (areHookInputsEqual(nextDeps, prevDeps)) {
                // 若是相等,返回舊的 callback
                return prevState[0];
            }
        }
    }
  
    hook.memoizedState = [callback, nextDeps];
    return callback;
}
複製代碼

核心邏輯就是比較deps是否發生變化,若是有變化則返回新的callback函數,不然返回原函數。其中比較方法areHookInputsEqual內部實際調用了 React 的is工具方法:

// 排除如下兩種特殊狀況:
// +0 === -0 // true
// NaN === NaN // false

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
  );
}
複製代碼

2.useMemo

2.1 不要濫用useMemo

import React, { useMemo } from 'react';

function Comp() {
    const v = 0;
    const memoV = useMemo(() => v, []);
    
    return <div>{memoV}</div>;
}
複製代碼

建立memoV的開銷是沒有必要的,緣由與第一節提到的相同。只有當建立行爲自己會產生高昂的開銷(好比計算上千次纔會生成變量值),纔有必要使用useMemo,固然這種場景少之又少。

2.2 useMemo的正確使用方法

前文咱們提到,優化 React 組件性能的兩個主要思路之一是減小計算量,這也是useMemo的用武之地:

import React, { useMemo } from 'react';

function Comp({ a, b }) {
    const v = 0;
    const calculate = (a, b) => {
        // ... complex calculation
        return c;
    }
    
    const memoV = useMemo((a, b) => v, [a, b]);
    
    return <div>{memoV}</div>;
}
複製代碼

3.總結

React Hook 對團隊的協做一致性要求很是高,useCallbackuseMemo這一對方法就是很好的示例,更復雜的場景還有對useRef、自定義 Hook 的使用等等。從經驗上來看,團隊在進行 Hook 編碼時須要特別增強 code review,不然很容易出現難以定位的 bug 或性能問題。當前 Hook 的各種方法還不完善,推特上爭論也不少,期待 React 後續版本提供出更成熟易用的方案。

相關文章
相關標籤/搜索