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

89

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

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

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

java-man-скрипт
источник
1
Не думаю, что тебе нужно звонить computedCallback = calcCallback();. computedCallbackдолжно быть просто = calcCallback , it will update the callback once neverChange`.
Noitidart 03
1
useCallback (fn, deps) эквивалентен useMemo (() => fn, deps).
Генри Лю

Ответы:

155

TL; DR;

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

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

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

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

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

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

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

пример

Предположим, у нас есть PureComponentдочерний элемент на основе -b, <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что для доступа к aпеременной используется закрытие , которое было записано при onPureChangeобъявлении. Чтобы исправить это, нам нужно сообщить React, где нужно разместить onPureChangeи воссоздать / запомнить (запоминать) новую версию, которая указывает на правильные данные. Мы делаем это, добавляя aв качестве зависимости второй аргумент к `useCallback:

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

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

NB не только для PureComponent/ React.memo, ссылочное равенство может иметь решающее значение при использовании чего-либо в качестве зависимости в useEffect.

Skyboyer
источник
19

Однострочный для useCallbackvs useMemo:

useCallback(fn, deps)это эквивалентно в useMemo(() => fn, deps).


С помощью useCallbackфункций useMemoMemoize запоминает любое вычисленное значение:

const fn = () => 42 // assuming expensive calculation here
const memoFn = useCallback(fn, [dep]) // (1)
const memoFnReturn = useMemo(fn, [dep]) // (2)

(1)вернет мемоизированную версию fn- одну и ту же ссылку для нескольких отрисовок, если depона одинакова. Но каждый раз, когда вы вызываете memoFn , это сложное вычисление начинается снова.

(2)будет вызывать fnкаждый раз при depизменении и запоминать возвращаемое значение ( 42здесь), которое затем сохраняется в memoFnReturn.

ford04
источник
18

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

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«ИНГ и addEventListenerING на элемент каждый раз , если только computedCallbackизменения. И computedCallbackменяется только при изменении переменных. Таким образом отреагировать будет только addEventListenerодин раз.

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

Noitidart
источник
2
просто небольшой комментарий к хорошему ответу: основная цель не в том addEventListener/removeEventListener(сама эта операция не тяжелая, поскольку не приводит к перекомпоновке / перерисовке DOM), а во избежание повторного рендеринга PureComponent(или с пользовательским shouldComponentUpdate()) дочерним
элементом,
Спасибо, @skyboyer, я понятия не имел о *EventListenerтом, что это дешево, это здорово, что он не вызывает перекомпоновку / рисование! Я всегда думал, что это дорого, поэтому старался избегать этого. Итак, в случае, если я не перехожу к a PureComponent, стоит ли прибавлять сложность useCallbackкомпромиссом в отношении реакции и дополнительной сложности DOM remove/addEventListener?
Noitidart 03
1
если не использовать PureComponentили настраивать shouldComponentUpdateдля вложенных компонентов, то useCallbackне добавит никакого значения (накладные расходы на дополнительную проверку второго useCallbackаргумента сведут к нулю пропуск дополнительного removeEventListener/addEventListenerхода)
skyboyer
Вау, супер интересно, спасибо, что поделились этим, это совершенно новый взгляд на то, *EventListenerчто для меня это не дорогая операция.
Noitidart 03
2

useMemo и useCallback используйте мемоизацию.

Мне нравится думать о мемоизации как о запоминании чего-то .

Хотя оба useMemoи useCallback что- то запоминают между рендерами до тех пор, пока зависимости не изменятся, разница только в том, что они помнят. .

useMemoбудет помнить , возвращаемое значение из вашей функции.

useCallbackбудет помнить фактическую функцию.

Источник: в чем разница между useMemo и useCallback?

Атомикты
источник