Яка різниця між useCallback та useMemo на практиці?


85

Можливо, я щось неправильно зрозумів, але useCallback Hook запускається кожного разу, коли відбувається повторне відображення.

Я передав вхідні дані - як другий аргумент useCallback - постійно мінливі константи, - але повернутий запам'ятаний зворотний виклик все одно запускає мої дорогі обчислення при кожному рендері (я впевнений - ви можете перевірити це самостійно у фрагменті нижче).

Я змінив useCallback на useMemo - і useMemo працює, як очікувалося - працює, коли передані вхідні зміни змінюються. І справді запам'ятовує дорогі розрахунки.

Живий приклад:

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


1
Не думаю, що вам потрібно телефонувати computedCallback = calcCallback();. computedCallbackмає бути просто = calcCallback , it will update the callback once neverChange` змінюється.
Noitidart

1
useCallback (fn, deps) еквівалентно useMemo (() => fn, deps).
Генрі Лю

Відповіді:


148

TL; DR;

  • useMemo полягає у запам'ятовуванні результату обчислення між викликами функції та між рендерами
  • useCallback полягає у запам'ятовуванні самого зворотного виклику (посилальна рівність) між рендерами
  • useRef полягає у збереженні даних між рендерами (оновлення не запускає рендерінг)
  • useState полягає у збереженні даних між рендерами (оновлення запускатиме рендерінг)

Довга версія:

useMemo зосереджується на уникненні важких розрахунків.

useCallbackфокусується на іншому: він виправляє проблеми з продуктивністю, коли вбудовані обробники подій, наприклад, onClick={() => { doSomething(...); }викликають PureComponentповторний візуалізацію дочірнього пристрою (оскільки вирази функцій там щоразу відносно різні)

Це сказано useCallbackближче useRef, ніж спосіб запам'ятовування результату розрахунку.

Вивчаючи документи, я згоден, що там виглядає заплутано.

useCallbackповерне запам'ятану версію зворотного дзвінка, яка змінюється лише у тому випадку, якщо змінився один із входів. Це корисно при передачі зворотних викликів оптимізованим дочірнім компонентам, які покладаються на посилальну рівність, щоб запобігти непотрібним відтворенням (наприклад, shouldComponentUpdate).

Приклад

Припустимо, у нас є PureComponentдочірня особа, <Pure />яка рендериться лише після propsзміни.

Цей код рендеріть дочірню інформацію кожного разу, коли батьківський рендерінг - оскільки вбудована функція щоразу посилається на іншу функцію:

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

Ми можемо впоратися з цим за допомогою useCallback:

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

Але після aзміни ми виявляємо, що onPureChangeстворена нами функція обробника - і React запам'яталася нам - все ще вказує на старе aзначення! У нас помилка замість проблеми з продуктивністю! Це тому, що onPureChangeвикористовує закриття для доступу до aзмінної, яка була захоплена при onPureChangeоголошенні. Щоб виправити це, нам потрібно повідомити React, куди його скинути, onPureChangeі заново створити / запам'ятати (запам'ятати) нову версію, яка вказує на правильні дані. Ми робимо це, додаючи aяк залежність у другому аргументі до `useCallback:

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

Тепер, якщо aйого змінили, React повторно відтворює компонент. І під час повторного відтворення він бачить, що залежність від onPureChangeвідрізняється, і існує потреба у створенні / запам'ятовуванні нової версії зворотного виклику. Нарешті все працює!


3
Дуже детальна та <Pure> відповідь, велике спасибі. ;)
RegarBoy

17

Ви зателефонуєте до нагадованого зворотного дзвінка щоразу, коли:

const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
const computedCallback = calcCallback();

Ось чому кількість useCallbackпіднімається. Однак функція ніколи не змінюється, вона ніколи ***** не створює **** новий зворотний виклик, завжди однаковий. Сенс useCallback- це правильно робити свою роботу.

Давайте внесемо деякі зміни у ваш код, щоб переконатися, що це правда. Давайте створимо глобальну змінну,, lastComputedCallbackяка буде відстежувати повернення нової (іншої) функції. Якщо повертається нова функція, це означає useCallbackпросто "виконати ще раз". Отже, коли він буде запущений знову, ми зателефонуємо expensiveCalc('useCallback'), оскільки так ви рахуєте, чи useCallbackспрацювало. Я роблю це в наведеному нижче коді, і тепер зрозуміло, що useCallbackзапам'ятовує, як очікувалося.

Якщо ви хочете бачити useCallbackповторне створення функції кожного разу, тоді розкомментируйте рядок у масиві, який проходить second. Ви побачите, що він заново створить функцію.

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

Перевага useCallbackв тому , що функція повертається те ж саме, так реагувати не НЕ removeEventListener«ІНГ і addEventListenerING на елемент кожен раз , якщо тільки computedCallbackзміни. І computedCallbackєдине змінюється, коли змінюються змінні. Таким чином реагувати буде лише addEventListenerодин раз.

Чудове питання, я дізнався багато нового, відповівши на нього.


2
лише невеликий коментар до гарної відповіді: основна мета не в тому, addEventListener/removeEventListener(сама ця робота не важка, оскільки не призводить до переформатування / перефарбовування DOM), а уникнути повторної рендеринга PureComponent(або за допомогою користувацького shouldComponentUpdate()) дочірнього, який використовує цей зворотний виклик
skyboyer

Дякую @skyboyer, я не мав уявлення про *EventListenerдешевизну, це чудовий момент, це не спричиняє переплавлення / фарби! Я завжди думав, що це дорого, тому намагався цього уникати. Тож у випадку, якщо я не переходжу до а PureComponent, чи додається складність useCallbackвартістю компромісу з реагуванням і DOM робить додаткову складність remove/addEventListener?
Noitidart

1
якщо не використовувати PureComponentабо настроювати shouldComponentUpdateдля вкладених компонентів, тоді useCallbackне додасть ніякого значення (накладні витрати шляхом додаткової перевірки на другий useCallbackаргументатор зведе нанівець пропуск додаткового removeEventListener/addEventListenerходу)
skyboyer

Вау надзвичайно цікаво дякую, що поділилися цим, це абсолютно новий погляд на те, як *EventListenerце не дорога операція для мене.
Noitidart

15

Один лайнер для useCallbackпроти useMemo:

useCallback(fn, deps)це еквівалентно в useMemo(() => fn, deps).


За useCallbackдопомогою функцій useMemoпам'яті запам'ятовує будь-яке обчислене значення:

const fn = () => 42 // assuming expensive calculation here
const memoFn = useCallback(fn, [dep]) // (1)
const memoFnReturn = useMemo(fn, [dep]) // (2)

(1)поверне зауважену версію fn- однакового посилання в декількох візуалізаціях, якщо depвоно однакове. Але кожного разу, коли ви викликаєте memoFn , це складне обчислення починається знову.

(2)буде викликати fnкожні depзміни та запам'ятовуватиме його повернене значення ( 42тут), яке потім зберігається у memoFnReturn.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.