Was ist der Unterschied zwischen useCallback und useMemo in der Praxis?

85

Vielleicht habe ich etwas falsch verstanden, aber useCallback Hook wird jedes Mal ausgeführt, wenn ein erneutes Rendern erfolgt.

Ich habe Eingaben übergeben - als zweites Argument für useCallback - nicht ständig änderbare Konstanten -, aber der zurückgegebene Rückruf führt immer noch meine teuren Berechnungen bei jedem Rendern aus (ich bin mir ziemlich sicher - Sie können dies im folgenden Snippet selbst überprüfen).

Ich habe useCallback in useMemo geändert - und useMemo funktioniert wie erwartet - wird ausgeführt, wenn sich übergebene Eingaben ändern. Und merkt sich wirklich die teuren Berechnungen.

Live-Beispiel:

'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-Skript
quelle
1
Ich glaube nicht, dass Sie anrufen müssen computedCallback = calcCallback();. computedCallbacksollte nur sein = calcCallback , it will update the callback once neverChange` ändert sich.
Noitidart
1
useCallback (fn, deps) entspricht useMemo (() => fn, deps).
Henry Liu

Antworten:

148

TL; DR;

  • useMemo dient zum Speichern eines Berechnungsergebnisses zwischen den Aufrufen einer Funktion und zwischen Rendern
  • useCallback besteht darin, einen Rückruf selbst (referenzielle Gleichheit) zwischen Renderings zu speichern
  • useRef dient dazu, Daten zwischen den Renderings zu speichern (beim Aktualisieren wird kein erneutes Rendern ausgelöst)
  • useState dient dazu, Daten zwischen den Renderings zu speichern (beim Aktualisieren wird ein erneutes Rendern ausgelöst).

Lange Version:

useMemo konzentriert sich auf die Vermeidung schwerer Berechnungen.

useCallbackkonzentriert sich auf eine andere Sache: Es behebt Leistungsprobleme, wenn Inline-Ereignishandler wie onClick={() => { doSomething(...); }das PureComponenterneute Rendern von Kindern verursachen (da Funktionsausdrücke dort jedes Mal referenziell unterschiedlich sind).

Dies useCallbackist useRefeher eine Möglichkeit, sich ein Berechnungsergebnis zu merken, als eine Möglichkeit, es sich zu merken.

Wenn ich mir die Dokumente anschaue , stimme ich zu, dass es dort verwirrend aussieht.

useCallbackgibt eine gespeicherte Version des Rückrufs zurück, die sich nur ändert, wenn sich einer der Eingänge geändert hat. Dies ist nützlich, wenn Rückrufe an optimierte untergeordnete Komponenten übergeben werden, die auf Referenzgleichheit beruhen, um unnötiges Rendern zu vermeiden (z. B. shouldComponentUpdate).

Beispiel

Angenommen, wir haben ein PureComponentKind auf Basis <Pure />, das erst dann neu gerendert wird, wenn propses geändert wird.

Dieser Code rendert das Kind jedes Mal neu, wenn das Elternteil neu gerendert wird - da die Inline-Funktion jedes Mal referenziell unterschiedlich ist:

function Parent({ ... }) {
  const [a, setA] = useState(0);
  ... 
  return (
    ...
    <Pure onChange={() => { doSomething(a); }} />
  );
}

Wir können das mit Hilfe von useCallback:

function Parent({ ... }) {
  const [a, setA] = useState(0);
  const onPureChange = useCallback(() => {doSomething(a);}, []);
  ... 
  return (
    ...
    <Pure onChange={onPureChange} />
  );
}

Sobald dies ageändert wurde, stellen wir fest, dass die von onPureChangeuns erstellte Handlerfunktion - und React für uns in Erinnerung geblieben - immer noch auf den alten aWert verweist ! Wir haben einen Fehler anstelle eines Leistungsproblems! Dies liegt daran, dass onPureChangeein Abschluss verwendet wird, um auf die aVariable zuzugreifen , die beim onPureChangeDeklarieren erfasst wurde. Um dies zu beheben, müssen wir React wissen lassen, wo onPureChangeeine neue Version abgelegt und neu erstellt / gespeichert (gespeichert) werden soll, die auf die richtigen Daten verweist. Wir tun dies, indem wir aals Abhängigkeit im zweiten Argument zu `useCallback hinzufügen:

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

Wenn dies ageändert wird, rendert React die Komponente neu. Beim erneuten Rendern wird festgestellt, dass die Abhängigkeit für onPureChangeunterschiedlich ist und eine neue Version des Rückrufs neu erstellt / gespeichert werden muss. Endlich funktioniert alles!

Skyboyer
quelle
3
Sehr detailliert und <Pure> Antwort, vielen Dank. ;)
RegarBoy
17

Sie rufen den gespeicherten Rückruf jedes Mal auf, wenn Sie Folgendes tun:

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

Deshalb useCallbacksteigt die Zahl der . Die Funktion ändert sich jedoch nie, sie erzeugt niemals einen neuen Rückruf, sie ist immer dieselbe. Das useCallbackheißt, es macht seinen Job richtig.

Nehmen wir einige Änderungen an Ihrem Code vor, um festzustellen, ob dies der Fall ist. Erstellen wir eine globale Variable, die verfolgt lastComputedCallback, ob eine neue (andere) Funktion zurückgegeben wird. Wenn eine neue Funktion zurückgegeben wird, bedeutet dies, dass useCallbacknur "erneut ausgeführt" wird. Wenn es erneut ausgeführt wird, rufen wir an expensiveCalc('useCallback'), da Sie auf diese Weise zählen, ob useCallbackes funktioniert hat. Ich mache dies im Code unten, und es ist jetzt klar, dass useCallbackdas Auswendiglernen wie erwartet ist.

Wenn Sie möchten, useCallbackdass die Funktion jedes Mal neu erstellt wird, kommentieren Sie die Zeile im übergebenen Array aus second. Sie werden sehen, dass die Funktion neu erstellt wird.

'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>

Vorteil der useCallbackist , dass die zurückgegebene Funktion ist die gleiche, so reagieren nicht removeEventListener‚ing und addEventListenering auf dem Element jedes Mal, sofern die computedCallbackÄnderungen. Und das computedCallbackändert sich nur, wenn sich die Variablen ändern. Somit wird nur addEventListenereinmal reagieren .

Tolle Frage, ich habe viel gelernt, indem ich sie beantwortet habe.

Noitidart
quelle
2
Nur ein kleiner Kommentar zur guten Antwort: Das Hauptziel ist nicht addEventListener/removeEventListener(diese Operation selbst ist nicht schwer, da sie nicht zu einem DOM-Reflow / Repaint führt), sondern zu vermeiden, dass ein Kind, das diesen Rückruf verwendet , erneut gerendert wird PureComponent(oder mit benutzerdefinierten shouldComponentUpdate())
skyboyer
Danke @skyboyer Ich hatte keine Ahnung *EventListener, billig zu sein, das ist ein großartiger Punkt, der keinen Reflow / Lack verursacht! Ich fand es immer teuer und versuchte es zu vermeiden. Wenn ich also nicht zu a übergehe PureComponent, ist die Komplexität, die durch useCallbackden Kompromiss zwischen Reaktion und DOM hinzugefügt wird , zusätzliche Komplexität wert remove/addEventListener?
Noitidart
1
Wenn verschachtelte Komponenten nicht verwendet PureComponentoder angepasst werden, wird kein Wert hinzugefügt (Overhead durch zusätzliche Überprüfung auf das zweite Argument macht das Überspringen eines zusätzlichen shouldComponentUpdateuseCallbackuseCallbackremoveEventListener/addEventListener
Zugs ungültig
Wow, super interessant, danke, dass du das geteilt hast. Es ist ein ganz neuer Blick darauf, dass *EventListeneres für mich keine teure Operation ist.
Noitidart
15

Einzeiler für useCallbackvs useMemo:

useCallback(fn, deps)ist äquivalent zu useMemo(() => fn, deps).


Mit Ihren Memoize- useCallbackFunktionen useMemomerkt man sich jeden berechneten Wert:

const fn = () => 42 // assuming expensive calculation here
const memoFn = useCallback(fn, [dep]) // (1)
const memoFnReturn = useMemo(fn, [dep]) // (2)

(1)gibt eine gespeicherte Version von fn- dieselbe Referenz über mehrere Renderings zurück, solange depdieselbe identisch ist. Aber jedes Mal , wenn Sie aufrufen memoFn , beginnt diese komplexe Berechnung erneut.

(2)wird bei fnjeder depÄnderung aufgerufen und merkt sich den zurückgegebenen Wert ( 42hier), der dann in gespeichert wird memoFnReturn.

ford04
quelle