Spamiętywanie - React.Memo vs Memoize
Nadszedł czas by rzucić okiem na różnice pomiędzy biblioteką spamiętywania (memoizacji), w naszym przypadku memoize-one, a funkcją memo dostarczaną przez React.
Po pierwsze, memoize-one została wybrana arbitralnie. Mogliśmy wybrać dowolną bibliotekę spamiętywania, taką jak reselect, lodash, czy ramda. Memoize-one to malutka biblioteka, która zapamiętuje tylko najnowsze argumenty i wyniki. Aby to zrozumieć, przyjrzymy się, co własciwie oznacza spamiętywanie.
W informatyce, spamiętywanie lub memoizacja to technika optymalizacyjna stosowana przede wszystkim w celu przyspieszenia programów komputerowych poprzez zapisywanie wyników kosztownych wywołań funkcji i zwracanie wyników z pamięci podręcznej, gdy te same wejścia wystąpią ponownie.
Przepiszmy sobie to na fragment kodu:
function add(a, b) {
return a + b;
}
add(1, 2) //result is 3
add(1, 2) //we already know the result, return 3
Jest to nieco arbitralny przykład, jak zwykle, ponieważ taka kalkulacja jest niewiarygodnie tania. Ale wyobraźmy sobie, że gdyby wykonanie było bardziej intensywne, zawierało czyszczenie danych lub mapowanie właściwości - takie obliczenie może zająć trochę czasu. Jeśli nasza funkcja jest spamiętywana, możemy rozpoznać, że argumenty te zostały już wcześniej przekazane, a tym samym zwrócić wynik.
Nasz obecny przykład zwraca ponownie funkcję, więc zróbmy to samo z memoize-one:
import memoize from 'memoize-one';
function add(a, b) {
console.log('add');
return a + b;
}
const memAdd = memoize(add);
console.log(memAdd(1, 2))
console.log(memAdd(1, 2))
Gdybyś uruchomił ten kod, wypisałby on coś takiego:
add
3
3
Wynik jest przewidywalny. Przy pierwszym wywołaniu funkcji dodawania, nie uruchomiliśmy jej wcześniej. Więc przechodzimy przez funkcję zwyczajnie, wypisujemy ciąg add, a następnie wynik. Jednak za drugim razem uruchomiliśmy już tę funkcję, więc tym razem już nie wchodzimy do niej i nie drukujemy ponownie add.
Wskazuje to na podstawową zasadę przy korzystaniu z funkcji spamiętujących / memoizujących. Należy ich używać tylko z czystymi funkcjami. Dla naszych celów, w naszej funkcji nie powinno być żadnych skutków ubocznych. Dla danego zestawu argumentów funkcji powinniśmy zawsze oczekiwać tego samego rezultatu.
Przyjrzyjmy się zatem sytuacji, w której to nieprawda:
let c = 1;
function sideEffectAdd(a, b) {
console.log('seAdd')
return a + b + c;
}
const memAdd = memoize(sideEffectAdd);
console.log(memAdd(1, 2))
console.log(memAdd(1, 2))
c++
console.log(memAdd(1, 2))
Nasz wynik wygląda tak:
seAdd
4
4
4
Zauważ, że używamy zmiennej poza zasięgiem naszej funkcji. Oznacza to, że funkcja ta nie jest czysta. Ostateczna liczba powinna wynosić 5, ale ponieważ wejście jest takie samo, a biblioteka zakłada, że funkcja ta jest czysta, nigdy nie uruchamia kodu wewnętrznego i zwraca nam oryginalny, nieprawidłowy wynik.
Aby uzyskać bardziej szczegółowy opis czystych funkcji, rzuć okiem na ten świetny artykuł.
A co z Memo?
Co to jest memo z React? W szczególności przyjrzymy się funkcji memo, która ma opakowywać element funkcyjny.
Stwórzmy podobny komponent Add jak w naszej poprzedniej funkcji:
import React, { memo } from "react";
import "./App.css";
const Add = memo(props => {
const result = props.number * 2;
console.log('component rendered')
return <div>Component - {result}</div>;
});
function App() {
return (
<div className="App">
<Add number={2} />
<Add number={2} />
<Add number={2} />
</div>
);
}
export default App;
Czego spodziewasz się w konsoli? Dostarczyliśmy te same props i robimy podobną matematykę jak w naszej funkcji add memoize-one. Oto wynik:
component rendered
component rendered
component rendered
Uwaga: Twój przykład może mieć zakreślone trzy, zamiast indywidualnych printów.
Dlaczego uruchomił komponent aż trzy razy, skoro wejście było za każdym razem takie samo? Cóż, React.memo próbuje wykonać spamiętywanie, ale nie do generowania komponentu, ale raczej do instancji komponentu. Oznacza to, że dla danego komponentu, jeśli jest próba renderowania tego samego komponentu, ale propsy się nie zmieniły, to zamiast tego będziemy renderować ten sam wynik, co ostatnim razem.
Spójrzmy na przykład:
import React, { memo, useState } from "react";
import "./App.css";
const Add = memo(props => {
const result = props.number * 2;
console.log('component rendered')
return <div>Component - {result}</div>;
});
function App() {
const [value, setValue] = useState(0);
console.log('outter component rendered');
return (
<div className="App">
<Add number={2} />
<button onClick={() => setValue(value + 1)}>
Click me
</button>
</div>
);
}
export default App;
Każde kliknięcie powoduje renderowanie w rodzicu.
Jak widać, mimo że komponent nadrzędny jest ponownie renderowany, komponent wewnętrzny nigdy nie jest ponownie renderowany. Dzieje się tak dlatego, że ma te same props, więc po prostu zwraca poprzedni wynik.
Na koniec
Tak więc memoize-one (podobnie jak większość bibliotek spamiętujących) zapamiętuje wynik danej funkcji dla zestawu argumentów, bez względu na to, gdzie następuje ostatnie wykonanie. React.memo, z drugiej strony, służy do spamiętywania pojedynczego wystąpienia komponentu przy próbie ponownego renderowania i nie będzie działać poza swoją instancją.
Zanim zakończymy tą szybką analizę, chciałbym zwrócić uwagę, że istnieje nowy hook useMemo, który działa podobnie jak memoize-one i ma na celu zapamiętanie danych funkcji w kontekście komponentu funkcyjnego. W rzeczywistości może on być używany w połączeniu z React.memo, zarówno do spamiętywania komponentu, jak i wszelkich funkcji wewnętrznych. Mam nadzieję, że to zmniejszy nieco zamieszanie.
Cały kod, którego użyłem w tym artykule jest dostępny na Githubie.
Oryginalny artykuł w języku angielskim można przeczytać tutaj.
Odniesienia i kontynuacja tematu
https://reactjs.org/docs/hooks-reference.html#usememo
https://github.com/DennyScott/memo-vs-memoize
https://www.freecodecamp.org/news/what-is-a-pure-function-in-javascript-acb887375dfe/
https://github.com/reduxjs/reselect
https://lodash.com/docs/4.17.11#memoize
https://ramdajs.com/docs/#memoizeWith