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>
computedCallback = calcCallback();
.computedCallback
solo debería ser = calcCallback, it will update the callback once
neverChange` cambia.Respuestas:
TL; DR;
useMemo
es memorizar el resultado de un cálculo entre las llamadas de una función y entre los rendersuseCallback
es memorizar una devolución de llamada en sí (igualdad referencial) entre rendersuseRef
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.useCallback
se centra en una cosa diferente: soluciona problemas de rendimiento cuando los controladores de eventos en línea comoonClick={() => { doSomething(...); }
causanPureComponent
la re-renderización de los niños (porque las expresiones de función son referencialmente diferentes cada vez)Dicho esto,
useCallback
está más cercauseRef
que una forma de memorizar el resultado de un cálculo.Mirando en el documentos, estoy de acuerdo en que parece confuso allí.
Ejemplo
Supongamos que tenemos un
PureComponent
hijo basado<Pure />
se volvería a representar solo una vez queprops
se 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
a
se cambia, encontramos que laonPureChange
función de controlador que creamos, y React recordó por nosotros, ¡todavía apunta ala
valor anterior! ¡Tenemos un error en lugar de un problema de rendimiento! Esto se debe a queonPureChange
utiliza un cierre para acceder a laa
variable, que fue capturada cuandoonPureChange
se declaró. Para solucionar este problema, debemos dejar que React sepa dónde colocaronPureChange
y volver a crear / recordar (memorizar) una nueva versión que apunte a los datos correctos. Lo hacemos agregandoa
como dependencia en el segundo argumento a `useCallback:const [a, setA] = useState(0); const onPureChange = useCallback(() => {doSomething(a);}, [a]);
Ahora, si
a
se cambia, React vuelve a renderizar el componente. Y durante la re-renderización, ve que la dependencia deonPureChange
es diferente, y es necesario volver a crear / memorizar una nueva versión de la devolución de llamada. ¡Finalmente todo funciona!fuente
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
useCallback
está aumentando. Sin embargo, la función nunca cambia, nunca crea **** una nueva devolución de llamada, siempre es la misma. El significadouseCallback
es 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 queuseCallback
simplemente se "ejecuta de nuevo". Entonces, cuando se ejecute nuevamente, lo llamaremosexpensiveCalc('useCallback')
, ya que así es como está contando siuseCallback
funcionó. Hago esto en el código a continuación, y ahora está claro queuseCallback
está memorizando como se esperaba.Si desea
useCallback
volver 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
useCallback
es que la función devuelta es la misma, por lo que no reaccioneremoveEventListener
'ING yaddEventListener
ing en el elemento cada vez, a menos que elcomputedCallback
cambio. Y loscomputedCallback
únicos cambios cuando cambian las variables. Así reaccionará soloaddEventListener
una vez.Gran pregunta, aprendí mucho respondiéndola.
fuente
addEventListener/removeEventListener
(esta operación en sí no es pesada, ya que no conduce al reflujo / repintado de DOM) sino a evitar volver a renderizarPureComponent
(o personalizarshouldComponentUpdate()
) al niño que usa esta devolución de llamada*EventListener
era 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 aPureComponent
, ¿la complejidad agregadauseCallback
vale la pena el intercambio de tener que reaccionar y DOM hacer una complejidad adicionalremove/addEventListener
?PureComponent
o no se personalizashouldComponentUpdate
para los componentes anidados, entoncesuseCallback
no agregará ningún valor (la sobrecarga mediante la verificación adicional del segundouseCallback
argumento anulará la omisión deremoveEventListener/addEventListener
movimiento adicional )*EventListener
no es una operación costosa para mí.Una línea para
useCallback
vsuseMemo
:Con las
useCallback
funciones de memorización,useMemo
memoriza 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 lafn
misma referencia en varias representaciones, siempre quedep
sea la misma. Pero cada vez que invocamemoFn
, ese complejo cálculo comienza de nuevo.(2)
invocaráfn
cada vez quedep
cambie y recordará su valor devuelto (42
aquí), que luego se almacena enmemoFnReturn
.Mostrar fragmento de código
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"));
<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>
fuente