
Was ist der Unterschied zwischen useCallBack und useMemo? Und warum erwarten useMemo und useCallback eine Funktion? Wenn Sie bereits mit React Hooks gearbeitet haben, haben Sie sich diese Fragen vielleicht schon gestellt.
Wir werden uns ansehen, wie sie sich voneinander unterscheiden.

Hinweis: Dieser Artikel setzt ein grundlegendes Verständnis von Hooks voraus. Sie sollten „Hooks at a Glance“gelesen haben.

Die React-Dokumentation besagt, dass useCallback:
Gibt einen memoisierten Callback zurück.
Und das useMemo:
Gibt einen memoisierten Wert zurück.
Mit anderen Worten, useCallback gibt Ihnen referentielle Gleichheit zwischen Renderings für Funktionen. Und useMemo gibt Ihnen referentielle Gleichheit zwischen Renderings für Werte.
useCallback und useMemo erwarten beide eine Funktion und ein Array von Abhängigkeiten. Der Unterschied ist, dass useCallback seine Funktion zurückgibt, wenn sich die Abhängigkeiten ändern, während useMemo seine Funktion aufruft und das Ergebnis zurückgibt.
Da JavaScript First-Class-Funktionenbesitzt, ist useCallback(fn, deps) äquivalent zu useMemo(() => fn, deps).
Möchten Sie dieses Tutorial in Aktion sehen? Schauen Sie es sich auf YouTube an!
Lassen Sie uns die obige abstrakte Beschreibung anhand einfacher Beispiele verstehen und erläutern. Ich werde alles durchgehen, aber Sie können gerne überspringen, was Sie bereits wissen.
Wir haben gesagt, dass Funktionen in JavaScript First-Class sind. Das bedeutet, dass Sie in JavaScript eine Funktion einem Wert zuweisen können.
// values
const number = 1;
const greeting = 'Hello';
// function declaration
function foo() {
return 'bar';
}
// named function expression - possible
// because functions are first-class
// and therefore usable as values
const otherFoo = function() {
return 'bar';
};
// using arrow functions and implicit return
const anotherFoo = () => 'bar';
number; // 1
greeting; // 'Hello'
foo(); // 'bar'
otherFoo(); // 'bar'
anotherFoo(); // 'bar'
Funktionen in Variablen zu speichern, ermöglicht es Ihnen, sie als (die kursiv geschriebenen Punkte sind wichtig für React Hooks):
Funktionen, die Funktionen zurückgeben oder Funktionen als Eingabe akzeptieren, werden Higher-Order Functions genannt.
const identity = x => x;
identity(1); // 1
// Used as Higher-Order Function
identity(foo);
// ƒ foo() {
// return 'bar';
// }
identity(foo)(); // 'bar'Wenn Sie JavaScript verwenden, wissen Sie wahrscheinlich, dass es zwei Arten von Gleichheitsoperatoren. == für abstrakte Gleichheit und === für strikte Gleichheit. Die Gleichheitsprüfung in JavaScript kann eigenartig sein, und es gibt bereits viele hervorragende Artikel zu diesem Thema. Lesen Sie diese für einen detaillierten Einblick, da ich hier nur die grundlegenden Fälle behandeln werde, die für useCallback und useMemo.
Referentielle Gleichheit verwendet den strikten Gleichheitsvergleich, der wahr ist, wenn die Operanden vom gleichen Typ sind und die Inhalte übereinstimmen.
const greeting = 'hello';
const otherGreeting = 'hello';
function foo() {
return 'bar';
}
const otherFoo = function() {
return `bar`;
};
const anotherFoo = () => 'bar';
function sameFoo() {
return 'bar';
}
const fooReference = foo;
'hello' === 'hello'; // true
greeting === otherGreeting; // true
foo === foo; // true
foo === otherFoo; // false
foo === anotherFoo; // false
foo === sameFoo; // false
foo === fooReference; // true
Beachten Sie, wie foo === sameFoo false ergibt false (im Gegensatz zu greeting === otherGreeting). Das liegt daran, dass
foo und sameFoo haben dieselbe Definition, referenzieren aber zwei verschiedene Objekte.
Memoization ist eine Methode, um die Leistung zu beschleunigen, indem Berechnungen reduziert werden.
Wenn Sie ein Ergebnis zum ersten Mal berechnen, speichern Sie es. Benötigen Sie das Ergebnis für dieselben Argumente erneut, verwenden Sie das gespeicherte, anstatt es neu zu berechnen.
Hier ist eine einfache memoize Funktion in JavaScript.
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = args.toString();
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
function add(a, b) {
return a + b;
}
const memoizedAdd = memoize(add);
console.log(memoizedAdd(2, 3)); // Calculates and caches result
console.log(memoizedAdd(2, 3)); // Returns cached result
Im obigen Beispiel speichern Sie das Ergebnis der Addition von 2 und 3. Bei jedem Aufruf nach dem ersten wird das zwischengespeicherte Ergebnis verwendet.
Die APIs von useCallback und useMemo sehen ähnlich aus. Beide akzeptieren eine Funktion und ein Array von Abhängigkeiten.
useCallback(fn, deps);
useMemo(fn, deps);
Was ist also der Unterschied? useCallback gibt seine Funktion unaufgerufen zurück damit Sie sie später aufrufen können, während useMemo seine Funktion aufruft und das Ergebnis zurückgibt.
function foo() {
return 'bar';
}
const memoizedCallback = useCallback(foo, []);
const memoizedResult = useMemo(foo, []);
memoizedCallback;
// ƒ foo() {
// return 'bar';
// }
memoizedResult; // 'bar'
memoizedCallback(); // 'bar'
memoizedResult(); // 🔴 TypeError

Randnotiz: Nun – da Sie First-Class-Funktionen kennen – sollten Sie verstehen, warum useCallback(fn, deps) entspricht useMemo(() => fn, deps).

In der Praxis sind die obigen Beispiele bedeutungslos. Ich habe sie nur zur Veranschaulichung der API angeführt. Wenn Sie eine Funktion an useCallback oder useMemodirekt übergeben und diese Funktion mit leeren Abhängigkeiten verwenden können, könnten Sie die Funktion außerhalb der Komponente definieren (und auf useCallback oder useMemo) verzichten. Daher werden Sie in der Praxis so etwas sehen.
function MyComponent({ foo, initial }) {
// ...
const memoizedCallback = useCallback(() => {
// do something with foo and bar
someFunc(foo, bar);
}, [foo, bar]);
const memoizedResult = useMemo(() => someOtherFunc(foo, bar), [
foo,
bar,
]);
// ...
}
useCallback nimmt normalerweise einen Inline-Callback entgegen, der die Funktion aufruft, die die Abhängigkeiten verwendet. Und useMemo nimmt eine „Erstellungsfunktion“ entgegen, die eine Funktion aufruft und deren Ergebnisse zurückgibt.
Noch eine Anmerkung: Das Folgende mag für Sie trivial sein, aber ich musste lange darüber nachdenken, um es zu verstehen. Der folgende Code ist falsch. Sie können nicht verwenden useCallback um Werte zu memoizen. Das bedeutet useCallback(fn(), deps) gibt es nicht.
function sum(a, b) {
console.log('sum() ran');
return a + b;
}
function App() {
const [val1, setVal1] = useState(0);
const [val2, setVal2] = useState(0);
const [name, setName] = useState('Jim');
const result = useCallback(sum(val1, val2), [val1, val2]);
return (
<div className="App">
<input
value={val1}
onChange={({ target }) =>
setVal1(parseInt(target.value || 0, 10))
}
/>
<input
value={val2}
onChange={({ target }) =>
setVal2(parseInt(target.value || 0, 10))
}
/>
<input
placeholder="Name"
value={name}
onChange={({ target }) => setName(target.value)}
/>
<p>{result}</p>
</div>
);
}
Jedes Mal, wenn Sie ändern namesum wird neu berechnet. useCallback und useMemo sind so nützlich, weil sie eine verzögerte Auswertung ermöglichen. result wird bei jedem Rendern sofort ausgewertet. Probieren Sie es aus und werfen Sie einen Blick in Ihre Konsole.
Warum also benötigen wir zwei Hooks, die dem Memoizen von Werten gewidmet sind?
Sie möchten verwenden useCallback oder useMemo immer dann, wenn Sie auf referenzielle Gleichheit zwischen Renders angewiesen sind. Ich verwende es hauptsächlich für useEffect, React.memo und useMemo um zu ersetzen shouldComponentUpdate aus React.PureComponent da die Abhängigkeiten dieser Hooks auf referenzielle Gleichheit geprüft werden.
Nehmen wir an, Sie haben eine Komponente, die bei einem gegebenen userId die Benutzerdaten anzeigt.
import React, { useEffect, useState } from 'react';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
function User({ userId }) {
const [user, setUser] = useState({ name: '', email: '' });
const fetchUser = async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser);
};
useEffect(() => {
fetchUser();
}, []);
return (
<ListItem dense divider>
<ListItemText primary={user.name} secondary={user.email} />
</ListItem>
);
}
export default User;
Können Sie den/die Fehler erkennen?
Wenn Sie eslint-plugin-react-hooks installiert, wird es Sie wegen des Weglassens von fetchUser aus useEffectsAbhängigkeiten bemängeln. Aber es gedankenlos hinzuzufügen, führt tatsächlich zu einer Endlosschleife!
In React werden Funktionen, die in Funktionskomponenten definiert sind, bei jedem Rendern neu erstellt aufgrund von Closures, was zu referenzieller Ungleichheit führt.
const fetchUser = async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser); // 🔴 setState triggers re-render
};
useEffect(() => {
fetchUser();
}, [fetchUser]); // fetchUser is a new function on every render
Es gibt zwei Möglichkeiten, dieses Problem zu beheben. Die eleganteste Lösung wäre zugegebenermaßen, fetchUser in useEffect zu verschieben und userId als Abhängigkeit hinzuzufügen.
// 1. Way to solve the infinite loop
useEffect(() => {
const fetchUser = async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser); // Triggers re-render, but ...
};
fetchUser();
}, [userId]); // ✅ ... userId stays the same.
Dies ist jedoch ein Artikel über useCallback und useMemo, also lösen wir das Problem mit Ersterem. (Und vielleicht möchten Sie die Funktion fetchUseran mehreren Stellen verwenden.)
Sie können fetchUser mit useCallback definieren, sodass die Funktion gleich bleibt, es sei denn, userId ändert sich.
// 2. Way to solve the infinite loop
const fetchUser = useCallback(async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser);
}, [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]); // ✅ fetchUser stays the same between renders
Und schließlich, nehmen wir an, Sie filtern alle Benutzer und zeigen sie in einer Liste an.
// Some FP magic 🧙🏼♂️
const filter = (f, arr) => arr.filter(f);
const prop = key => obj => obj[key];
const getName = prop('name');
const strIncludes = query => str => str.includes(query);
const toLower = str => str.toLowerCase();
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const nameIncludes = query =>
pipe(
getName,
toLower,
strIncludes(toLower(query))
);
function UserList({ query, users }) {
// 🔴 Recalculated on every render
const filteredUsers = filter(nameIncludes(query), users);
// ...
}
Das ist ineffizient. Wir können useMemo nutzen, um die gefilterten Benutzer nur neu zu berechnen, wenn sich die Abfrage ändert.
function UserList({ query, users }) {
// ✅ Recalculated when query or users change
const filteredUsers = useMemo(
() => filter(nameIncludes(query), users),
[query, users]
);
// ...
}
useCallback kannst du eine Funktion definieren, die zwischen den Renderings referenzielle Gleichheit aufweist.useMemo verwenden, um einen Wert zu berechnen, der zwischen den Renderings referenzielle Gleichheit aufweist.Beide ändern ihren Rückgabewert nur, wenn sich ihre Abhängigkeiten ändern.
Wenn dir dieser Artikel gefallen hat, solltest du abonnieren meinen YouTube-Kanal 🤗