¿Cuál es la diferencia entre useCallback y useMemo en la práctica?

85

Tal vez no entendí algo, pero useCallback Hook se ejecuta cada vez que se vuelve a renderizar.

Pasé entradas, como segundo argumento para useCallback, constantes que no se cambian siempre, pero la devolución de llamada memorizada aún ejecuta mis costosos cálculos en cada render (estoy bastante seguro, puede verificarlo usted mismo en el fragmento a continuación).

He cambiado useCallback a useMemo, y useMemo funciona como se esperaba, se ejecuta cuando las entradas pasadas cambian. Y realmente memoriza los costosos cálculos.

Ejemplo en 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>

java-man-script
fuente
1
No creo que necesites llamar computedCallback = calcCallback();. computedCallbacksolo debería ser = calcCallback , it will update the callback once neverChange` cambia.
Noitidart
1
useCallback (fn, deps) es equivalente a useMemo (() => fn, deps).
Henry Liu

Respuestas:

148

TL; DR;

  • useMemo es memorizar el resultado de un cálculo entre las llamadas de una función y entre los renders
  • useCallback es memorizar una devolución de llamada en sí (igualdad referencial) entre renders
  • useRef es mantener los datos entre renderizados (la actualización no dispara el re-renderizado)
  • useState es mantener los datos entre renderizados (la actualización activará el re-renderizado)

Versión larga:

useMemo se centra en evitar cálculos pesados.

useCallbackse centra en una cosa diferente: soluciona problemas de rendimiento cuando los controladores de eventos en línea como onClick={() => { doSomething(...); }causan PureComponentla re-renderización de los niños (porque las expresiones de función son referencialmente diferentes cada vez)

Dicho esto, useCallbackestá más cerca useRefque una forma de memorizar el resultado de un cálculo.

Mirando en el documentos, estoy de acuerdo en que parece confuso allí.

useCallbackdevolverá una versión memorizada de la devolución de llamada que solo cambia si una de las entradas ha cambiado. Esto es útil cuando se pasan devoluciones de llamada a componentes secundarios optimizados que dependen de la igualdad de referencia para evitar representaciones innecesarias (por ejemplo, shouldComponentUpdate).

Ejemplo

Supongamos que tenemos un PureComponenthijo basado<Pure /> se volvería a representar solo una vez que propsse cambia.

Este código vuelve a renderizar al hijo cada vez que se vuelve a renderizar al padre, porque la función en línea es referencialmente diferente cada vez:

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

Podemos manejar eso con la ayuda de useCallback:

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

Pero una vez que ase cambia, encontramos que la onPureChangefunción de controlador que creamos, y React recordó por nosotros, ¡todavía apunta al avalor anterior! ¡Tenemos un error en lugar de un problema de rendimiento! Esto se debe a que onPureChangeutiliza un cierre para acceder a la avariable, que fue capturada cuando onPureChangese declaró. Para solucionar este problema, debemos dejar que React sepa dónde colocar onPureChangey volver a crear / recordar (memorizar) una nueva versión que apunte a los datos correctos. Lo hacemos agregando acomo dependencia en el segundo argumento a `useCallback:

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

Ahora, si ase cambia, React vuelve a renderizar el componente. Y durante la re-renderización, ve que la dependencia de onPureChangees diferente, y es necesario volver a crear / memorizar una nueva versión de la devolución de llamada. ¡Finalmente todo funciona!

Skyboyer
fuente
3
Respuesta muy detallada y <Pura>, muchas gracias. ;)
RegarBoy
17

Está llamando a la devolución de llamada memorizada cada vez, cuando lo hace:

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

Es por eso que el conteo de useCallbackestá aumentando. Sin embargo, la función nunca cambia, nunca crea **** una nueva devolución de llamada, siempre es la misma. El significado useCallbackes hacer correctamente su trabajo.

Hagamos algunos cambios en su código para ver que esto es cierto. Creemos una variable global lastComputedCallback, que hará un seguimiento de si se devuelve una función nueva (diferente). Si se devuelve una nueva función, eso significa que useCallbacksimplemente se "ejecuta de nuevo". Entonces, cuando se ejecute nuevamente, lo llamaremos expensiveCalc('useCallback'), ya que así es como está contando si useCallbackfuncionó. Hago esto en el código a continuación, y ahora está claro queuseCallback está memorizando como se esperaba.

Si desea useCallbackvolver a crear la función cada vez, descomente la línea en la matriz que pasasecond . Verá que vuelve a crear la función.

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

Beneficio de useCallbackes que la función devuelta es la misma, por lo que no reaccione removeEventListener'ING y addEventListenering en el elemento cada vez, a menos que el computedCallbackcambio. Y los computedCallbackúnicos cambios cuando cambian las variables. Así reaccionará solo addEventListeneruna vez.

Gran pregunta, aprendí mucho respondiéndola.

Noitidart
fuente
2
solo un pequeño comentario para una buena respuesta: el objetivo principal no se trata de addEventListener/removeEventListener(esta operación en sí no es pesada, ya que no conduce al reflujo / repintado de DOM) sino a evitar volver a renderizar PureComponent(o personalizar shouldComponentUpdate()) al niño que usa esta devolución de llamada
skyboyer
Gracias @skyboyer. No tenía idea de que *EventListenerera barato, ¡es un gran punto que no cause reflujo / pintura! Siempre pensé que era caro, así que intenté evitarlo. Entonces, en el caso de que no pase a a PureComponent, ¿la complejidad agregada useCallbackvale la pena el intercambio de tener que reaccionar y DOM hacer una complejidad adicional remove/addEventListener?
Noitidart
1
si no se usa PureComponento no se personaliza shouldComponentUpdatepara los componentes anidados, entonces useCallbackno agregará ningún valor (la sobrecarga mediante la verificación adicional del segundo useCallbackargumento anulará la omisión de removeEventListener/addEventListenermovimiento adicional )
skyboyer
Wow super interesante gracias por compartir esto, es una mirada completamente nueva sobre cómo *EventListenerno es una operación costosa para mí.
Noitidart
15

Una línea para useCallbackvs useMemo:

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


Con las useCallbackfunciones de memorización, useMemomemoriza cualquier valor calculado:

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

(1)devolverá una versión memorizada de la fnmisma referencia en varias representaciones, siempre que depsea ​​la misma. Pero cada vez que invoca memoFn , ese complejo cálculo comienza de nuevo.

(2)invocará fncada vez que depcambie y recordará su valor devuelto ( 42aquí), que luego se almacena en memoFnReturn.

ford04
fuente