Какая разница между useCallback и useMemo на практике?

Может быть, я что-то неправильно понял, но useCallback Hook запускается каждый раз, когда происходит повторный рендеринг.

Я передал входные данные - в качестве второго аргумента для использования CallCall - неизменяемые константы - но возвращенный запомненный обратный вызов по-прежнему запускает мои дорогостоящие вычисления при каждом рендере (я почти уверен - вы можете проверить это самостоятельно во фрагменте ниже).

Я изменил useCallback на useMemo - и useMemo работает как положено - запускается при изменении пропущенных входных данных. И действительно запоминает дорогие расчеты.

Живой пример:

'use strict';

const { useState, useCallback, useMemo } = React;

const neverChange = 'I never change';
const oneSecond = 1000;

function App() {
  const [second, setSecond] = useState(0);
  
  // This 👇 expensive function executes everytime when render happens:
  const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
  const computedCallback = calcCallback();
  
  // This 👇 executes once
  const computedMemo = useMemo(() => expensiveCalc('useMemo'), [neverChange]);
  
  setTimeout(() => setSecond(second + 1), oneSecond);
  
  return '
    useCallback: ${computedCallback} times |
    useMemo: ${computedMemo} |
    App lifetime: ${second}sec.
  ';
}

const tenThousand = 10 * 1000;
let expensiveCalcExecutedTimes = { 'useCallback': 0, 'useMemo': 0 };

function expensiveCalc(hook) {
  let i = 0;
  while (i < tenThousand) i++;
  
  return ++expensiveCalcExecutedTimes[hook];
}


ReactDOM.render(
  React.createElement(App),
  document.querySelector('#app')
);
<h1>useCallback vs useMemo:</h1>
<div id="app">Loading...</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

Ответ 1

TL; DR;

  • useMemo - useMemo результат расчета между вызовами функций и между рендерами.
  • useCallback должен useCallback сам обратный вызов (ссылочное равенство) между рендерами
  • useRef - хранить данные между useRef (обновление не useRef рендеринг)
  • useState - хранить данные между useState (обновление запустит повторный рендеринг).

Длинная версия:

useMemo сосредотачивается на том, чтобы избежать тяжелого вычисления.

useCallback фокусируется на другом: он устраняет проблему производительности, когда встроенные обработчики событий, такие как onClick={() => { doSomething(...); } onClick={() => { doSomething(...); } делает PureComponent дочерним для повторного рендеринга (так как выражение функции там каждый раз ссылочно отличается)

При этом useCallback ближе к useRef а не к useRef результата вычислений.

Изучая документы, я согласен, что это выглядит странно.

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

пример

Предположим, у нас есть PureComponent элемент PureComponent -based <Pure/> который будет перерисован только после изменения его props

Следующий код переопределяет child каждый раз, когда parent переопределяется - так как встроенная функция референциально отличается каждый раз

function Parent({ ... }) {
  const [a, setA] = useState(0);
  ... 
  return (
    ...
    <Pure onChange={() => { doSomething(a); }} />
  );
}

Мы можем справиться с этим с помощью useCallback

function Parent({ ... }) {
  const [a, setA] = useState(0);
  const onPureChange = useCallback(() => {doSomething(a);});
  ... 
  return (
    ...
    <Pure onChange={onPureChange} />
  );
}

Но как только меняется, мы находим, что a onPureChange мы создали и React запомнился нам по- прежнему указывает на старой a стоимости! У нас ошибка вместо проблемы с производительностью! Это связано с тем, что onPureChange использует функцию закрытия для доступа к переменным (не для доступа по имени переменной). Чтобы сделать это правильно, нам нужно дать React знать, куда onPureChange и заново создать/запомнить (запомнить) новую версию, которая указывает на правильные данные. И здесь нам нужен второй аргумент:

const [a, setA] = useState(0);
const onPureChange = useCallback(() => {doSomething(a);}, [a]);

Теперь, если изменяются Реагировать повторно рендеры компонента (это a useState движения). И во время повторного рендеринга он обнаруживает, что входные данные для onPureChange отличаются, и возникает необходимость заново создать/запомнить новую версию обратного вызова. Наконец-то и все работает!

Ответ 2

Вы вызываете запомненный обратный вызов каждый раз, когда делаете:

const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
const computedCallback = calcCallback();

Вот почему подсчет useCallback увеличивается. Однако функция никогда не меняется, она никогда не создает ***** новый обратный вызов, он всегда один и тот же. Значение useCallback правильно делает свою работу.

Давайте внесем некоторые изменения в ваш код, чтобы убедиться, что это правда. Давайте создадим глобальную переменную lastComputedCallback, которая будет отслеживать, будет ли возвращена новая (другая) функция. Если возвращается новая функция, это означает, что useCallback просто "выполняется снова". Поэтому, когда он снова expensiveCalc('useCallback') мы будем вызывать expensiveCalc('useCallback'), поскольку именно так вы рассчитываете, работает ли useCallback. Я делаю это в приведенном ниже коде, и теперь ясно, что useCallback запоминает, как и ожидалось.

Если вы хотите, чтобы useCallback каждый раз заново создавал функцию, то раскомментируйте строку в массиве, которая проходит через second. Вы увидите, как это воссоздает функцию.

'use strict';

const { useState, useCallback, useMemo } = React;

const neverChange = 'I never change';
const oneSecond = 1000;

let lastComputedCallback;
function App() {
  const [second, setSecond] = useState(0);
  
  // This 👇 is not expensive, and it will execute every render, this is fine, creating a function every render is about as cheap as setting a variable to true every render.
  const computedCallback = useCallback(() => expensiveCalc('useCallback'), [
    neverChange,
    // second // uncomment this to make it return a new callback every second
  ]);
  
  
  if (computedCallback !== lastComputedCallback) {
    lastComputedCallback = computedCallback
    // This 👇 executes everytime computedCallback is changed. Running this callback is expensive, that is true.
    computedCallback();
  }
  // This 👇 executes once
  const computedMemo = useMemo(() => expensiveCalc('useMemo'), [neverChange]);
  
  setTimeout(() => setSecond(second + 1), oneSecond);
  return '
    useCallback: ${expensiveCalcExecutedTimes.useCallback} times |
    useMemo: ${computedMemo} |
    App lifetime: ${second}sec.
  ';
}

const tenThousand = 10 * 1000;
let expensiveCalcExecutedTimes = { 'useCallback': 0, 'useMemo': 0 };

function expensiveCalc(hook) {
  let i = 0;
  while (i < 10000) i++;
  
  return ++expensiveCalcExecutedTimes[hook];
}


ReactDOM.render(
  React.createElement(App),
  document.querySelector('#app')
);
<h1>useCallback vs useMemo:</h1>
<div id="app">Loading...</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>