Jaka jest różnica między useCallback i useMemo w praktyce?

86

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>

java-man-script
źródło
1
Myślę, że nie musisz dzwonić computedCallback = calcCallback();. computedCallbackpowinno być po prostu = calcCallback , it will update the callback once neverChange` changes.
Noitidart
1
useCallback (fn, deps) jest równoważne użyciu useMemo (() => fn, deps).
Henry Liu

Odpowiedzi:

149

TL; DR;

  • useMemo jest zapamiętanie wyniku obliczenia między wywołaniami funkcji i między renderami
  • useCallback polega na zapamiętaniu samego wywołania zwrotnego (równości referencyjnej) między renderami
  • useRef 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ń.

useCallbackkoncentruje się na innej rzeczy: rozwiązuje problemy z wydajnością, gdy wbudowane programy obsługi zdarzeń, takie jak onClick={() => { doSomething(...); }powodują PureComponentponowne renderowanie potomne (ponieważ wyrażenia funkcji są referencyjnie różne za każdym razem)

To powiedziawszy, useCallbackjest bliższe useRef, a nie sposób na zapamiętanie wyniku obliczeń.

Patrząc na dokumenty , zgadzam się, że wygląda to zagmatwane.

useCallbackzwróci zapamiętaną wersję wywołania zwrotnego, która zmienia się tylko wtedy, gdy zmieniło się jedno z wejść. Jest to przydatne podczas przekazywania wywołań zwrotnych do zoptymalizowanych komponentów potomnych, które opierają się na równości odwołań, aby zapobiec niepotrzebnym renderowaniu (np. ShouldComponentUpdate).

Przykład

Załóżmy, że mamy PureComponentdziecko oparte na systemie , <Pure />które renderuje się ponownie tylko wtedy, gdy propszostanie 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 azmianie okazuje się, że onPureChangefunkcja obsługi, którą stworzyliśmy - a React zapamiętał dla nas - nadal wskazuje na starą awartość! Zamiast problemu z wydajnością mamy błąd! Dzieje się tak, ponieważ onPureChangeużywa zamknięcia, aby uzyskać dostęp do azmiennej, która została przechwycona podczas onPureChangezadeklarowania. Aby to naprawić, musimy powiadomić Reacta, gdzie upuścić onPureChangei ponownie utworzyć / zapamiętać (zapamiętać) nową wersję, która wskazuje na prawidłowe dane. Robimy to, dodając ajako zależność w drugim argumencie do `useCallback:

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

Teraz, jeśli azostanie zmieniony, React ponownie renderuje komponent. Podczas ponownego renderowania widzi, że zależność dla onPureChangejest inna i istnieje potrzeba ponownego utworzenia / zapamiętania nowej wersji wywołania zwrotnego. Wreszcie wszystko działa!

skyboyer
źródło
3
Bardzo szczegółowa i <Czysta> odpowiedź, wielkie dzięki. ;)
RegarBoy
17

Dzwonisz do zapamiętanego oddzwonienia za każdym razem, gdy:

const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
const computedCallback = calcCallback();

Dlatego liczba useCallbackrośnie. Jednak funkcja nigdy się nie zmienia, nigdy ***** nie tworzy **** nowego wywołania zwrotnego, jest zawsze takie samo. Znaczenie useCallbackto 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 to useCallbackpo prostu „wykonanie ponownie”. Więc kiedy wykona się ponownie, zadzwonimy expensiveCalc('useCallback'), ponieważ tak liczysz, czy useCallbackzadziałało. Robię to w poniższym kodzie i teraz jest jasne, że useCallbackzapamiętuje się zgodnie z oczekiwaniami.

Jeśli chcesz, aby useCallbackfunkcja była odtwarzana za każdym razem, odkomentuj wiersz w tablicy, która przechodzi second. 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ą useCallbackjest to, że funkcja zwracany jest ten sam, to nie reaguje removeEventListener„ing i addEventListenering na elemencie za każdym razem, chyba że computedCallbackzmiany. I computedCallbackzmienia się tylko wtedy, gdy zmieniają się zmienne. W ten sposób zareaguje tylko addEventListenerraz.

Świetne pytanie, wiele się nauczyłem, odpowiadając na nie.

Noitidart
źródło
2
tylko mały komentarz do dobrej odpowiedzi: głównym celem nie jest addEventListener/removeEventListener(sama operacja nie jest ciężka, ponieważ nie prowadzi do ponownego PureComponentshouldComponentUpdate()
wlania
Dzięki @skyboyer Nie miałem pojęcia *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 a PureComponent, czy złożoność dodana jest useCallbackwarta kompromisu posiadania reakcji, a DOM robi dodatkową złożoność remove/addEventListener?
Noitidart
1
jeśli nie zostanie użyty PureComponentlub niestandardowy shouldComponentUpdatedla zagnieżdżonych komponentów useCallback, nie doda żadnej wartości (dodatkowe sprawdzenie drugiego useCallbackargumgenta anuluje pominięcie dodatkowego removeEventListener/addEventListenerruchu)
skyboyer
Wow, super interesujące, dziękuję za udostępnienie tego, to zupełnie nowe spojrzenie na *EventListenerto, jak nie jest to dla mnie kosztowna operacja.
Noitidart
15

Jedna linijka dla useCallbackVS useMemo:

useCallback(fn, deps)jest równoważny do useMemo(() => fn, deps).


Dzięki useCallbackfunkcji useMemomemoize 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ę tego fnsamego odwołania w wielu renderach , o ile depjest taka sama. Ale za każdym razem , gdy wywołujesz memoFn , te złożone obliczenia zaczynają się od nowa.

(2)wywoła fnkażdą depzmianę i zapamięta jego zwróconą wartość ( 42tutaj), która jest następnie przechowywana w memoFnReturn.

ford04
źródło