Qual è la differenza tra useCallback e useMemo nella pratica?


89

Forse ho frainteso qualcosa, ma useCallback Hook viene eseguito ogni volta che si verifica un nuovo rendering.

Ho passato gli input - come secondo argomento per useCallback - costanti non modificabili - ma il callback memorizzato restituito esegue ancora i miei calcoli costosi ad ogni rendering (sono abbastanza sicuro - puoi controllare da solo nello snippet qui sotto).

Ho cambiato useCallback in useMemo - e useMemo funziona come previsto - viene eseguito quando gli input passati cambiano. E memorizza davvero i calcoli costosi.

Esempio dal vivo:

'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
Non credo che tu abbia bisogno di chiamare computedCallback = calcCallback();. computedCallbackdovrebbe essere solo = calcCallback , it will update the callback once neverChange` cambia.
Noitidart

1
useCallback (fn, deps) è equivalente a useMemo (() => fn, deps).
Henry Liu

Risposte:


155

TL; DR;

  • useMemo è memorizzare un risultato di calcolo tra le chiamate di una funzione e tra i rendering
  • useCallback è quello di memorizzare un callback stesso (uguaglianza referenziale) tra i rendering
  • useRef è conservare i dati tra i rendering (l'aggiornamento non attiva il ri-rendering)
  • useState è conservare i dati tra i rendering (l'aggiornamento attiverà il nuovo rendering)

Versione lunga:

useMemo si concentra sull'evitare calcoli pesanti.

useCallback si concentra su una cosa diversa: risolve i problemi di prestazioni quando ai gestori di eventi inline piace onClick={() => { doSomething(...); } causano PureComponentil ri-rendering del figlio (perché le espressioni di funzione sono referenzialmente diverse ogni volta)

Detto questo, useCallback è più vicino useRef, piuttosto che un modo per memorizzare un risultato di calcolo.

Esaminando i documenti sono d'accordo che sembra confuso lì.

useCallbackrestituirà una versione memorizzata del callback che cambia solo se uno degli input è cambiato. Ciò è utile quando si passano callback a componenti figlio ottimizzati che si basano sull'uguaglianza dei riferimenti per evitare rendering non necessari (ad esempio shouldComponentUpdate).

Esempio

Supponiamo di avere un PureComponentfiglio basato su- <Pure />che potrebbe ri-renderizzare solo una volta che propsè stato modificato.

Questo codice esegue nuovamente il rendering del figlio ogni volta che viene eseguito nuovamente il rendering del genitore, poiché la funzione inline è referenzialmente diversa ogni volta:

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

Possiamo gestirlo con l'aiuto di useCallback:

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

Ma una volta amodificato, scopriamo che la onPureChangefunzione handler che abbiamo creato - e che React ha ricordato per noi - punta ancora al vecchio avalore! Abbiamo un bug invece di un problema di prestazioni! Questo perché onPureChangeutilizza una chiusura per accedere alla avariabile, che è stata acquisita quando è onPureChangestata dichiarata. Per risolvere questo problema, dobbiamo far sapere a React dove rilasciare onPureChangee ricreare / ricordare (memoize) una nuova versione che punti ai dati corretti. Lo facciamo aggiungendo acome dipendenza nel secondo argomento a `useCallback:

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

Ora, se aviene modificato, React esegue nuovamente il rendering del componente. E durante il ri-rendering, vede che la dipendenza per onPureChangeè diversa, e c'è la necessità di ricreare / memorizzare una nuova versione della richiamata. Finalmente tutto funziona!

NB non solo per PureComponent/ React.memo, l'uguaglianza referenziale può essere critica quando si usa qualcosa come dipendenza in useEffect.


19

One-liner per useCallbackvs useMemo:

useCallback(fn, deps)è equivalente a useMemo(() => fn, deps).


Con le useCallbackfunzioni di memoize, useMemomemorizza qualsiasi valore calcolato:

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

(1)restituirà una versione memoizzata di fn- stesso riferimento su più rendering, purché depsia lo stesso. Ma ogni volta che invocate memoFn , quel complesso calcolo ricomincia.

(2)richiamerà fnogni volta che depcambia e ricorderà il suo valore restituito ( 42qui), che viene quindi memorizzato in memoFnReturn.


18

Stai chiamando la richiamata memorizzata ogni volta, quando:

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

Questo è il motivo per cui il conteggio di useCallbacksta aumentando. Tuttavia la funzione non cambia mai, non ***** crea mai **** un nuovo callback, è sempre lo stesso. SensouseCallback sta facendo correttamente il suo lavoro.

Apportiamo alcune modifiche al codice per vedere che ciò è vero. Creiamo una variabile globale lastComputedCallback, che terrà traccia se viene restituita una nuova funzione (diversa). Se viene restituita una nuova funzione, significa useCallbacksemplicemente "eseguita di nuovo". Quindi, quando verrà eseguito di nuovo, chiameremo expensiveCalc('useCallback'), poiché è così che conti se useCallbackha funzionato. Lo faccio nel codice seguente ed è ora chiaro che useCallbackmemorizza come previsto.

Se vuoi vedere useCallbackricreare la funzione ogni volta, rimuovi il commento dalla riga nell'array che passa second. Lo vedrai ricreare la funzione.

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

Il vantaggio di useCallbackè che la funzione restituita è la stessa, quindi non si reagisce removeEventListenere si addEventListenerinterviene sull'elemento ogni volta, A MENO che non vengano computedCallbackapportate modifiche. E le computedCallbackuniche modifiche quando cambiano le variabili. Quindi reagirà solo addEventListeneruna volta.

Ottima domanda, ho imparato molto rispondendo.


2
solo un piccolo commento per una buona risposta: l'obiettivo principale non riguarda addEventListener/removeEventListener(questa operazione in sé non è pesante poiché non porta a ridisegnare / ridipingere DOM) ma evitare di ri-renderizzare PureComponent(o con personalizzato shouldComponentUpdate()) il bambino che usa questo callback
skyboyer

Grazie @skyboyer non avevo idea di *EventListeneressere economico, questo è un ottimo punto per non causare reflow / paint! Ho sempre pensato che fosse costoso, quindi ho cercato di evitarlo. Quindi, nel caso in cui non passo ad a PureComponent, la complessità aggiunta useCallbackvale il compromesso di aver reagito e DOM fare una complessità extra remove/addEventListener?
Noitidart

1
se non usi PureComponento personalizzato shouldComponentUpdateper i componenti nidificati useCallback, non aggiungerà alcun valore (il controllo aggiuntivo per il secondo useCallbackargumgent annullerà il salto di removeEventListener/addEventListenermosse extra )
skyboyer

Wow super interessante grazie per aver condiviso questo, è uno sguardo completamente nuovo su come *EventListenernon sia un'operazione costosa per me.
Noitidart

2

useMemoe useCallbackusa la memoizzazione.

Mi piace pensare alla memoizzazione come al ricordo di qualcosa .

Mentre entrambi useMemoe useCallback ricordano qualcosa tra i rendering finché le dipendenze non cambiano, la differenza è proprio ciò che ricordano .

useMemosi ricorderà il valore restituito da una funzione.

useCallbackvi ricorderà la vostra funzione attuale.

Fonte: qual è la differenza tra useMemo e useCallback?

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.