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

Возможно, я что-то неправильно понял, но 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>

Я не думаю, что вам нужно звонить computedCallback = calcCallback();. computedCallback должно быть просто = calcCallback, it will update the callback once neverChange`изменения.

Noitidart 03.03.2019 03:17

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

Henry Liu 06.02.2020 16:36

Всякий раз, когда вы объявляете функцию со скобками, она вызывает функцию. В результате при каждом обновлении second будет выполняться функция calcCallback.

Carlos Emiliano Castro Trejo 30.03.2021 06:02
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
143
3
48 570
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ответ принят как подходящий

TL;ДР;

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

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

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

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

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

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

useCallback will return a memoized version of the callback that only changes if one of the inputs has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. 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 использует замыкание для доступа к переменной a, которая была захвачена при объявлении onPureChange. Чтобы исправить это, нам нужно сообщить React, куда бросить onPureChange, и воссоздать/запомнить (запоминать) новую версию, которая указывает на правильные данные. Мы делаем это, добавляя a в качестве зависимость во второй аргумент `useCallback:

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

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

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

Я не думаю, что этот комментарий очень хорошо связал все воедино. Означает ли добавление массива обратного вызова и зависимостей, что <Pure/> не будет перерисовываться, если a не изменился?

maxflow 27.02.2021 11:00

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

skyboyer 27.02.2021 12:14
because the inline function is referentially different each time это то, что действительно довело меня до точки зрения. Спасибо!!
steve 04.06.2021 08:59

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

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 один раз.

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

просто небольшой комментарий к хорошему ответу: основная цель не в addEventListener/removeEventListener(эта операция сама по себе не тяжелая, поскольку не приводит к перекомпоновке/перерисовке DOM), а в том, чтобы избежать повторного рендеринга PureComponent(или с помощью пользовательского shouldComponentUpdate()) дочернего элемента, который использует этот обратный вызов

skyboyer 03.03.2019 10:12

Спасибо @skyboyer, я понятия не имел о том, что *EventListener стоит дешево, это здорово, что это не вызывает оплавление / рисование! Я всегда думал, что это дорого, поэтому я старался избегать этого. Итак, в случае, если я не перехожу к PureComponent, стоит ли сложность, добавленная useCallback, компромисса с реагированием и дополнительной сложностью DOM remove/addEventListener?

Noitidart 03.03.2019 14:05

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

skyboyer 03.03.2019 14:29

Вау, супер интересно, спасибо, что поделились этим, это совершенно новый взгляд на то, как *EventListener не дорогая операция для меня.

Noitidart 03.03.2019 14:58

Один вкладыш для useCallback vs useMemo:

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).


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

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.

const App = () => {
  const [dep, setDep] = useState(0);
  const fn = () => 42 + dep; // assuming expensive calculation here
  const memoFn = useCallback(fn, [dep]); // (1)
  const memoFnReturn = useMemo(fn, [dep]); // (2)

  return (
    <div>
      <p> memoFn is {typeof memoFn} </p>
      <p>
        Every call starts new calculation, e.g. {memoFn()} {memoFn()}
      </p>
      <p>memoFnReturn is {memoFnReturn}</p>
      <p>
        Only one calculation for same dep, e.g. {memoFnReturn} {memoFnReturn}
      </p>
      <button onClick = {() => setDep((p) => p + 1)}>Change dep</button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback, useMemo } = React</script>

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

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

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

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

useCallback будет запомнить вашей фактической функцией.

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

Замечательный ответ

Mir Stephen 05.09.2021 13:32

Мы можем использовать **useCallback** для запоминания функции, что означает, что эта функция будет переопределена только в том случае, если какая-либо из ее зависимостей в массиве зависимостей изменится.

**useMemo(() => computation(a, b), [a, b])** — это хук, который позволяет нам запоминать дорогостоящие вычисления. Учитывая те же зависимости [a, b], после запоминания хук будет возвращать запомненное значение без вызова вычисления (a, b).

This article about different React Memoization Methods: React.memo, useMemo, useCallback, what is different between them with practical examples: https://medium.com/geekculture/great-confusion-about-react-memoization-methods-react-memo-usememo-usecallback-a10ebdd3a316

UseMemo используется для запоминания значений, React.memo используется для переноса компонентов React Function для предотвращения повторного рендеринга. useCallback используется для запоминания функций.

Другие вопросы по теме