Какая разница между 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>