Może coś źle zrozumiałem, ale useCallback Hook działa za każdym razem, gdy ma miejsce ponowne renderowanie.
Przekazałem dane wejściowe - jako drugi argument do useCallback - stałe, których nie można zmieniać - ale zwrócone zapamiętane wywołanie zwrotne nadal uruchamia moje drogie obliczenia przy każdym renderowaniu (jestem prawie pewien - możesz to sprawdzić samodzielnie w poniższym fragmencie).
Zmieniłem useCallback na useMemo - a useMemo działa zgodnie z oczekiwaniami - działa po przekazaniu zmian wejściowych. I naprawdę zapamiętuje drogie obliczenia.
Przykład na żywo:
'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
powinno być po prostu = calcCallback, it will update the callback once
neverChange` changes.Odpowiedzi:
TL; DR;
useMemo
jest zapamiętanie wyniku obliczenia między wywołaniami funkcji i między renderamiuseCallback
polega na zapamiętaniu samego wywołania zwrotnego (równości referencyjnej) między renderamiuseRef
jest przechowywanie danych między renderami (aktualizacja nie uruchamia ponownego renderowania)useState
jest zachowanie danych między renderami (aktualizacja spowoduje ponowne renderowanie)Długa wersja:
useMemo
koncentruje się na unikaniu ciężkich obliczeń.useCallback
koncentruje się na innej rzeczy: rozwiązuje problemy z wydajnością, gdy wbudowane programy obsługi zdarzeń, takie jakonClick={() => { doSomething(...); }
powodująPureComponent
ponowne renderowanie potomne (ponieważ wyrażenia funkcji są referencyjnie różne za każdym razem)To powiedziawszy,
useCallback
jest bliższeuseRef
, a nie sposób na zapamiętanie wyniku obliczeń.Patrząc na dokumenty , zgadzam się, że wygląda to zagmatwane.
Przykład
Załóżmy, że mamy
PureComponent
dziecko oparte na systemie ,<Pure />
które renderuje się ponownie tylko wtedy, gdyprops
zostanie zmienione.Ten kod renderuje ponownie dziecko za każdym razem, gdy rodzic jest ponownie renderowany - ponieważ funkcja wbudowana jest referencyjnie inna za każdym razem:
function Parent({ ... }) { const [a, setA] = useState(0); ... return ( ... <Pure onChange={() => { doSomething(a); }} /> ); }
Poradzimy sobie z tym przy pomocy
useCallback
:function Parent({ ... }) { const [a, setA] = useState(0); const onPureChange = useCallback(() => {doSomething(a);}, []); ... return ( ... <Pure onChange={onPureChange} /> ); }
Ale po
a
zmianie okazuje się, żeonPureChange
funkcja obsługi, którą stworzyliśmy - a React zapamiętał dla nas - nadal wskazuje na starąa
wartość! Zamiast problemu z wydajnością mamy błąd! Dzieje się tak, ponieważonPureChange
używa zamknięcia, aby uzyskać dostęp doa
zmiennej, która została przechwycona podczasonPureChange
zadeklarowania. Aby to naprawić, musimy powiadomić Reacta, gdzie upuścićonPureChange
i ponownie utworzyć / zapamiętać (zapamiętać) nową wersję, która wskazuje na prawidłowe dane. Robimy to, dodająca
jako zależność w drugim argumencie do `useCallback:const [a, setA] = useState(0); const onPureChange = useCallback(() => {doSomething(a);}, [a]);
Teraz, jeśli
a
zostanie zmieniony, React ponownie renderuje komponent. Podczas ponownego renderowania widzi, że zależność dlaonPureChange
jest inna i istnieje potrzeba ponownego utworzenia / zapamiętania nowej wersji wywołania zwrotnego. Wreszcie wszystko działa!źródło
Dzwonisz do zapamiętanego oddzwonienia za każdym razem, gdy:
const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]); const computedCallback = calcCallback();
Dlatego liczba
useCallback
rośnie. Jednak funkcja nigdy się nie zmienia, nigdy ***** nie tworzy **** nowego wywołania zwrotnego, jest zawsze takie samo. ZnaczenieuseCallback
to poprawnie wykonuje swoją pracę.Zróbmy kilka zmian w kodzie, aby zobaczyć, że to prawda. Stwórzmy zmienną globalną
lastComputedCallback
, która będzie śledzić, czy zostanie zwrócona nowa (inna) funkcja. Jeśli zwracana jest nowa funkcja, oznacza touseCallback
po prostu „wykonanie ponownie”. Więc kiedy wykona się ponownie, zadzwonimyexpensiveCalc('useCallback')
, ponieważ tak liczysz, czyuseCallback
zadziałało. Robię to w poniższym kodzie i teraz jest jasne, żeuseCallback
zapamiętuje się zgodnie z oczekiwaniami.Jeśli chcesz, aby
useCallback
funkcja była odtwarzana za każdym razem, odkomentuj wiersz w tablicy, która przechodzisecond
. Zobaczysz, jak odtworzy funkcję.'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>
Zaletą
useCallback
jest to, że funkcja zwracany jest ten sam, to nie reagujeremoveEventListener
„ing iaddEventListener
ing na elemencie za każdym razem, chyba żecomputedCallback
zmiany. IcomputedCallback
zmienia się tylko wtedy, gdy zmieniają się zmienne. W ten sposób zareaguje tylkoaddEventListener
raz.Świetne pytanie, wiele się nauczyłem, odpowiadając na nie.
źródło
addEventListener/removeEventListener
(sama operacja nie jest ciężka, ponieważ nie prowadzi do ponownegoPureComponent
shouldComponentUpdate()
*EventListener
, że jestem tani, to świetna uwaga, jeśli chodzi o to, aby nie powodować rozpływu / farby! Zawsze uważałem, że to drogie, więc starałem się tego uniknąć. Więc w przypadku, gdy nie przechodzę do aPureComponent
, czy złożoność dodana jestuseCallback
warta kompromisu posiadania reakcji, a DOM robi dodatkową złożonośćremove/addEventListener
?PureComponent
lub niestandardowyshouldComponentUpdate
dla zagnieżdżonych komponentówuseCallback
, nie doda żadnej wartości (dodatkowe sprawdzenie drugiegouseCallback
argumgenta anuluje pominięcie dodatkowegoremoveEventListener/addEventListener
ruchu)*EventListener
to, jak nie jest to dla mnie kosztowna operacja.Jedna linijka dla
useCallback
VSuseMemo
:Dzięki
useCallback
funkcjiuseMemo
memoize zapamiętuje każdą obliczoną wartość:const fn = () => 42 // assuming expensive calculation here const memoFn = useCallback(fn, [dep]) // (1) const memoFnReturn = useMemo(fn, [dep]) // (2)
(1)
zwróci zapamiętaną wersję tegofn
samego odwołania w wielu renderach , o iledep
jest taka sama. Ale za każdym razem , gdy wywołujeszmemoFn
, te złożone obliczenia zaczynają się od nowa.(2)
wywołafn
każdądep
zmianę i zapamięta jego zwróconą wartość (42
tutaj), która jest następnie przechowywana wmemoFnReturn
.Pokaż fragment kodu
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>
źródło