86
votes

Quelle est la différence entre useCallback et useMemo en pratique?

Peut-être que j'ai mal compris quelque chose, mais useCallback Hook s'exécute à chaque fois que le rendu se produit.

J'ai passé des entrées - comme deuxième argument à useCallback - des constantes non toujours modifiables - mais le rappel mémorisé renvoyé exécute toujours mes calculs coûteux à chaque rendu (je suis presque sûr - vous pouvez vérifier par vous-même dans l'extrait ci-dessous).

J'ai changé useCallback pour useMemo - et useMemo fonctionne comme prévu - s'exécute lorsque les entrées passées changent. Et mémorise vraiment les calculs coûteux.

Exemple en direct:

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


2 commentaires

Je ne pense pas que vous ayez besoin d'appeler computedCallback = calcCallback(); . computedCallback devrait juste être = calcCallback , it will update the callback once neverChange` change.


useCallback (fn, deps) équivaut à useMemo (() => fn, deps).


4 Réponses :


151
votes

TL, DR;

  • useMemo consiste à mémoriser un résultat de calcul entre les appels d'une fonction et entre les rendus
  • useCallback consiste à mémoriser un callback lui-même (égalité référentielle) entre les rendus
  • useRef est de conserver les données entre les rendus (la mise à jour ne déclenche pas le re-rendu)
  • useState consiste à conserver les données entre les rendus (la mise à jour déclenchera le nouveau rendu)

Version longue:

useMemo vise à éviter les calculs lourds.

useCallback se concentre sur une chose différente: il corrige les problèmes de performances lorsque les gestionnaires d'événements en ligne comme onClick={() => { doSomething(...); } provoquer le re-rendu de l'enfant PureComponent (car les expressions de fonction sont référentiellement différentes à chaque fois)

Cela dit, useCallback est plus proche de useRef que d'un moyen de mémoriser un résultat de calcul.

En regardant dans la documentation, je suis d'accord que cela semble déroutant.

useCallback renverra une version mémorisée du rappel qui ne change que si l'une des entrées a changé. Ceci est utile lorsque vous passez des rappels à des composants enfants optimisés qui reposent sur l'égalité de référence pour éviter les rendus inutiles (par exemple, shouldComponentUpdate).

Exemple

Supposons que nous ayons un enfant <Pure /> basé sur PureComponent qui ne ferait un nouveau rendu qu'une fois ses props modifiés.

Ce code restitue l'enfant chaque fois que le parent est rendu à nouveau - car la fonction en ligne est référentiellement différente à chaque fois:

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

Nous pouvons gérer cela avec l'aide de useCallback :

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

Mais une fois a est changé , nous constatons que la onPureChange fonction de gestionnaire , nous avons créé â € » et réagir pour nous rappeler € » encore des points à l'ancienne d' a valeur! Nous avons un bug au lieu d'un problème de performances! En effet , onPureChange utilise une fermeture pour accéder à la a variable qui a été capturé quand onPureChange a été déclarée. Pour résoudre ce problème, nous devons indiquer à React où déposer onPureChange et recréer / mémoriser (mémoriser) une nouvelle version qui pointe vers les données correctes. Nous le faisons en ajoutant a comme dépendance dans le deuxième argument de `useCallback:

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

Désormais, si a est modifié, React restitue le composant. Et lors du re-rendu, il voit que la dépendance pour onPureChange est différente et qu'il est nécessaire de recréer / mémoriser une nouvelle version du rappel. Enfin tout fonctionne!


1 commentaires

Réponse très détaillée et <Pure>, merci beaucoup. ;)



17
votes

Vous appelez le rappel mémorisé à chaque fois, lorsque vous faites:

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

C'est pourquoi le nombre de useCallback augmente. Cependant, la fonction ne change jamais, elle ne crée jamais ***** de nouveau rappel, c'est toujours le même. useCallback signifie que useCallback fait correctement son travail.

Faisons quelques changements dans votre code pour voir que c'est vrai. Créons une variable globale, lastComputedCallback , qui suivra si une nouvelle fonction (différente) est renvoyée. Si une nouvelle fonction est renvoyée, cela signifie que useCallback vient d'être "exécuté à nouveau". Donc, quand il s'exécutera à nouveau, nous appellerons expensiveCalc('useCallback') , car c'est ainsi que vous useCallback si useCallback a fonctionné. Je fais cela dans le code ci-dessous, et il est maintenant clair que useCallback mémorise comme prévu.

Si vous voulez voir useCallback recréer la fonction à chaque fois, décommentez la ligne du tableau qui passe en second . Vous le verrez recréer la fonction.

'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')
);
const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
const computedCallback = calcCallback();

L'avantage de useCallback est que la fonction retournée est la même, donc removeEventListener n'est pas removeEventListener 'ing et addEventListener ing sur l'élément à chaque fois, SAUF les changements computedCallback . Et le computedCallback ne change que lorsque les variables changent. Ainsi, addEventListener qu'une seule fois.

Excellente question, j'ai beaucoup appris en y répondant.


4 commentaires

juste un petit commentaire pour une bonne réponse: l'objectif principal ne concerne pas addEventListener/removeEventListener (cette opération en elle-même n'est pas lourde puisqu'elle ne conduit pas à redistribuer / repeindre DOM) mais d'éviter de re-rendre PureComponent (ou avec la coutume shouldComponentUpdate() ) enfant qui utilise ce rappel


Merci @skyboyer Je n'avais aucune idée du fait que *EventListener était bon marché, c'est un excellent point de ne pas provoquer de reflux / peinture! J'ai toujours pensé que c'était cher alors j'ai essayé de l'éviter. Donc, dans le cas où je ne passe pas à un PureComponent , la complexité ajoutée par useCallback vaut-elle le compromis d'avoir réagi et que DOM fait une complexité supplémentaire remove/addEventListener ?


si ne pas utiliser PureComponent ou personnalisés shouldComponentUpdate pour les composants imbriqués alors useCallback n'ajoutera une valeur (frais généraux par vérification supplémentaire pour la deuxième useCallback argumgent invalidera sauter supplémentaire removeEventListener/addEventListener mouvement)


Wow super intéressant merci pour ce partage, c'est un tout nouveau regard sur la façon dont *EventListener n'est pas une opération coûteuse pour moi.



16
votes

One-liner pour useCallback vs useMemo :

useCallback(fn, deps) équivaut à useMemo(() => fn, deps) .


Avec useCallback vous mémorisez des fonctions, useMemo mémorise toute valeur calculée:

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

(1) renverra une version mémorisée de fn - même référence sur plusieurs rendus, tant que dep est le même. Mais chaque fois que vous invoquez memoFn , ce calcul complexe commence à nouveau.

(2) invoquera fn chaque fois que dep change et se souviendra de sa valeur retournée ( 42 ici), qui est ensuite stockée dans memoFnReturn .

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"));
const fn = () => 42 // assuming expensive calculation here
const memoFn = useCallback(fn, [dep]) // (1)
const memoFnReturn = useMemo(fn, [dep]) // (2)


0 commentaires

1
votes

useMemo et useCallback utilisent la useCallback .

J'aime penser que la mémorisation se souvient de quelque chose .

Bien que useMemo et useCallback souviennent de quelque chose entre les rendus jusqu'à ce que les dépendances changent, la différence est juste ce dont ils se souviennent .

useMemo se souviendra de la valeur renvoyée par votre fonction.

useCallback se souviendra de votre fonction réelle.

Source: Quelle est la différence entre useMemo et useCallback?


0 commentaires