Анализ memo, useMemo и useCallback

React.js

предисловие

До появления хуков, если компонент содержал внутреннийstate, мы основываемся наclassформы для создания компонентов.

В реакции точка оптимизации производительности:

  1. передачаsetState, это вызовет повторный рендеринг компонента, независимо от того, до или послеstateЭто то же самое
  2. При обновлении родительского компонента дочерний компонент также будет автоматически обновлен.

Основываясь на двух вышеупомянутых пунктах, наше обычное решение: используйтеimmutableСравните, позвоните, когда они не равныsetState, существуетshouldComponentUpdateдо и после судаpropsа такжеstate, если нет изменений, вернутьсяfalseчтобы предотвратить обновление.

существуетhooksПосле выхода в компоненте нет функцииshouldComponentUpdateВ жизненном цикле мы не можем решить, следует ли обновляться, оценивая состояние до и после.useEffectбольше не различатьmount updateДва состояния, а это значит, что при каждом вызове функционального компонента будет выполняться вся его внутренняя логика, что принесет большую потерю производительности.

В сравнении

Давайте кратко рассмотрим сигнатуры вызовов 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будет использоваться для обработки побочных эффектов, а первые два хука - нет.

useCallbackа такжеuseMemoбудет выполняться при первом рендеринге компонента, а затем выполняться снова, когда переменные зависят от изменения; и оба хука возвращают кешированное значение,useMemoвозврат в кэшеПеременная,useCallbackвозврат в кэшефункция

React.memo()

В эпоху классовых компонентов для оптимизации производительности мы часто используемPureComponent, каждый раз неглубокое сравнение пропсов, конечно кроме PureComponent можно ещеshouldComponentUpdateдля более глубокого контроля.

В функциональном компоненте React тесно предоставляетReact.memoЭтот HOC (компонент более высокого порядка) подобен PureComponent, но он специально предусмотрен для функционального компонента и не применим к классовому компоненту.

Но по сравнению с PureComponent,React.memo()может поддерживать указание参数, что может быть эквивалентноshouldComponentUpdateПоэтому React.memo() удобнее в использовании, чем PureComponent.

(Конечно, если вы сами инкапсулируете HOC и реализуете комбинацию PureComponent + shouldComponentUpdate внутри, все должно быть в порядке. В предыдущих проектах было довольно много способов ее использования.)

Сначала посмотрите, как используется 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);

Использование очень простое, вне функционального компонента объявитеareEqualметод судить дваждыpropsКакая разница, если не передать второй параметр, по умолчанию будет делаться только props浅比较

Последний экспортируемый компонент — это компонент, обернутый 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.stepа такжеprops.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Метод оценивается только тогда, когда реквизит дважды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);

Сравнение эффектов

Как видно из рисунка выше, при нажатии step и count изменились как props.step, так и props.count, поэтомуChild.jsЭтот дочерний компонент перерисовывается каждый раз (----re-render----), даже если эти два реквизита не используются.

В этом случае,ChildMemo.jsПовторный рендеринг повторно выполняться не будет.

Только при изменении props.number,ChildMemo.jsа такжеChild.jsПроизводительность стабильна.

Как видно из вышеизложенного,Второй метод React.memo() должен существовать для конкретного требования.Потому что в экспериментальном сценарии мы видим, что даже если я используюReact.memoОбертывание Child.js всегда вызывает повторный рендеринг, потому что поверхностное сравнение свойств должно было измениться.

Детальная оптимизация производительности React.useMemo()

Из приведенного выше использования React.memo() мы можем обнаружить, что, в конце концов, весь компонент обернут в самый внешний слой, и нам нужно вручную написать метод для сравнения этих конкретных реквизитов перед повторным рендерингом.

В некоторых сценариях мы просто хотим, чтобы часть компонента не перерисовывалась, а не весь компонент не перерисовывался, то есть для достижения局部 PureФункция.

useMemo()Основное использование заключается в следующем:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo() возвращает запомненное значение, которое пересчитывается только при изменении зависимостей (таких как a и b выше)

Если запомненное значение остается неизменным, логика рендеринга не будет перезапущена.

Говоря о логике рендеринга, следует помнить, что useMemo() выполняется во время рендеринга, поэтому некоторые дополнительные побочные операции, такие как сетевые запросы, выполняться не могут.

Если массив зависимостей не указан ([a,b] выше), то мемоизированное значение будет пересчитываться каждый раз, что также будет перекрашиваться.

Добавьте новый к приведенному выше коду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() используется для обёртывания логики возвращаемой части рендеринга, а объявление зависит от props.number, а остальные не изменились.

Эффект контраста:

На приведенном выше рисунке мы видим, что каждый раз, когда родительский компонент обновляет шаг/число, он запускает повторную визуализацию дочернего компонента, инкапсулированного с помощью useMemo, но число не меняется, указывая на то, что повторная визуализация HTML часть не была перезапущена

Только когда зависимый props.number изменится, повторный рендеринг внутри пакета useMemo() будет запущен повторно.

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.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, хотя они полезны внутри компонентов.React.memoоптимизировать, но мы заявляемhandleClickButton1Метод определяется напрямую, что означает, что пока родительский компонент повторно отображается (обновление состояния или свойств), здесь будет объявлен новый метод.Хотя новый метод и старый метод одинаковы по длине, они еще два разных объекта,React.memoПосле сравнения обнаруживается, что реквизит объекта изменился, и он перерисовывается.

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

В приведенном выше коде наш метод оборачивает слой с помощью useCallback, а затем передает[count2]переменная, здесь useCallback решит, следует ли возвращать новую функцию в зависимости от того, изменился ли count2, и соответственно будет обновлена ​​внутренняя область действия функции.

Так как наш метод зависит только от переменной count2, а count2 будет обновляться только после нажатия Button2handleClickButton2, поэтому мы нажимаем Button1 без повторного рендеринга содержимого Button2.

Суммировать

  1. В случае, если дочернему компоненту не нужны значения и функции родительского компонента, нужно использовать толькоmemoФункция оборачивает дочерний компонент.

  2. Если есть функция, переданная дочернему компоненту, используйтеuseCallback

  3. Если есть значение для передачи дочернему компоненту, используйтеuseMemo

  4. useEffect,useMemo,useCallbackобесобственное закрытиеиз. То есть каждый раз, когда компонент рендерится, он фиксирует состояние в контексте текущей функции компонента (state, props), поэтому каждый раз выполнение этих трех хуков отражаетсяТекущее состояние, вы не можете использовать их для захвата последнего состояния. Для этого случая мы должны использоватьrefпосетить.