Возможно, я что-то неправильно понял, но 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`.Ответы:
TL; DR;
useMemo
- запоминать результат вычислений между вызовами функции и между отрисовкамиuseCallback
состоит в том, чтобы запоминать сам обратный вызов (ссылочное равенство) между рендерамиuseRef
- хранить данные между рендерами (обновление не запускает повторный рендеринг)useState
- хранить данные между рендерингами (обновление вызовет повторный рендеринг)Длинная версия:
useMemo
фокусируется на избежании тяжелых расчетов.useCallback
фокусируется на другом: он исправляет проблемы с производительностью, когда встроенные обработчики событий, например,onClick={() => { doSomething(...); }
вызываютPureComponent
повторный рендеринг дочерних элементов (потому что выражения функций каждый раз по ссылкам разные)При этом,
useCallback
ближе кuseRef
, а не способ memoize результата вычисления.Изучая документы, я согласен, что там это выглядит запутанным.
пример
Предположим, у нас есть
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
.источник
Однострочный для
useCallback
vsuseMemo
:С помощью
useCallback
функцийuseMemo
Memoize запоминает любое вычисленное значение: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>
источник
Вы вызываете мемоизированный обратный вызов каждый раз, когда делаете:
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
ING на элемент каждый раз , если толькоcomputedCallback
изменения. ИcomputedCallback
меняется только при изменении переменных. Таким образом отреагировать будет толькоaddEventListener
один раз.Отличный вопрос, я многому научился, отвечая на него.
источник
addEventListener/removeEventListener
(сама эта операция не тяжелая, поскольку не приводит к перекомпоновке / перерисовке DOM), а во избежание повторного рендерингаPureComponent
(или с пользовательскимshouldComponentUpdate()
) дочерним*EventListener
том, что это дешево, это здорово, что он не вызывает перекомпоновку / рисование! Я всегда думал, что это дорого, поэтому старался избегать этого. Итак, в случае, если я не перехожу к aPureComponent
, стоит ли прибавлять сложностьuseCallback
компромиссом в отношении реакции и дополнительной сложности DOMremove/addEventListener
?PureComponent
или настраиватьshouldComponentUpdate
для вложенных компонентов, тоuseCallback
не добавит никакого значения (накладные расходы на дополнительную проверку второгоuseCallback
аргумента сведут к нулю пропуск дополнительногоremoveEventListener/addEventListener
хода)*EventListener
что для меня это не дорогая операция.useMemo
иuseCallback
используйте мемоизацию.Мне нравится думать о мемоизации как о запоминании чего-то .
Хотя оба
useMemo
иuseCallback
что- то запоминают между рендерами до тех пор, пока зависимости не изменятся, разница только в том, что они помнят. .useMemo
будет помнить , возвращаемое значение из вашей функции.useCallback
будет помнить фактическую функцию.Источник: в чем разница между useMemo и useCallback?
источник