В чем разница между useCallback и useMemo на практике? - PullRequest
0 голосов
/ 03 марта 2019

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

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

Я изменил 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>

Ответы [ 2 ]

0 голосов
/ 03 марта 2019

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

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

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

Давайте внесем некоторые изменения в ваш код, чтобы убедиться, что это правда.Давайте создадим глобальную переменную lastComputedCallback, которая будет отслеживать, будет ли возвращена новая (другая) функция.Если возвращается новая функция, это означает, что 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>

Преимущество useCallback заключается в том, что возвращаемая функция одинакова, поэтому реакция не removeEventListener и addEventListener на элемент каждый раз, ЕСЛИ computedCallback не изменится.И computedCallback изменяется только при изменении переменных.Таким образом реагировать будет только addEventListener один раз.

Отличный вопрос, я многому научился, ответив на него.

0 голосов
/ 03 марта 2019

TL; DR;

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

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

useMemo фокусируется на избежании тяжелых вычислений.

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

Сказанное useCallback ближе к useRef скореедля запоминания результата вычислений.

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

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

Пример

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

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

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 изменено, React повторно рендерит компонент (это движение useState).И во время повторного рендеринга он обнаруживает, что входные данные для onPureChange отличаются, и возникает необходимость заново создать / запомнить новую версию обратного вызова.Наконец-то и все работает!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...