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.
<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') );
4 Réponses :
TL, DR;
useMemo
consiste à mémoriser un résultat de calcul entre les appels d'une fonction et entre les rendususeCallback
consiste à mémoriser un callback lui-même (égalité référentielle) entre les rendususeRef
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!
Réponse très détaillée et <Pure>, merci beaucoup. ;)
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.
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.
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)
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?
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).