Pratikte useCallback ve useMemo arasındaki fark nedir?


89

Belki bir şeyi yanlış anladım, ancak yeniden oluşturma gerçekleştiğinde Geri Çağırma Kancası her seferinde çalışır.

Geri çağırmayı kullanmak için ikinci bir argüman olarak girdileri geçtim - hiç değişmeyen sabitler - ancak geri dönen geri çağırma, her işlemede pahalı hesaplamalarımı çalıştırmaya devam ediyor (oldukça eminim - aşağıdaki ön bilgide kendiniz kontrol edebilirsiniz).

UseCallback öğesini useMemo olarak değiştirdim - ve useMemo beklendiği gibi çalışıyor - giriş değişiklikleri geçildiğinde çalışır. Ve gerçekten pahalı hesaplamaları hatırlıyor.

Canlı örnek:

'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
Aramana gerek olduğunu sanmıyorum computedCallback = calcCallback();. computedCallbacksadece = calcCallback , it will update the callback once neverChange` değişiklikleri olmalıdır.
Noitidart

1
useCallback (fn, deps), useMemo (() => fn, deps) ile eşdeğerdir.
Henry Liu

Yanıtlar:


155

TL; DR;

  • useMemo bir fonksiyonun çağrıları arasında ve renderlar arasında bir hesaplama sonucunu hafızaya almaktır
  • useCallback renderlar arasında bir geri aramanın kendisini (referans eşitliği) hafızaya almaktır
  • useRef verileri oluşturmalar arasında tutmaktır (güncelleme, yeniden oluşturmayı tetiklemez)
  • useState verileri oluşturmalar arasında tutmaktır (güncelleme, yeniden oluşturmayı tetikler)

Uzun versiyon:

useMemo ağır hesaplamalardan kaçınmaya odaklanır.

useCallbackfarklı bir şeye odaklanır: satır içi olay işleyicileri gibi alt yeniden oluşturmaya onClick={() => { doSomething(...); }neden olduğunda performans sorunlarını düzeltir PureComponent(çünkü işlev ifadeleri her seferinde referans olarak farklıdır)

Bu, bir hesaplama sonucunu ezberlemenin bir yolundan ziyade daha useCallbackyakındır useRef.

Dokümanlara baktığımda, orada kafa karıştırıcı göründüğüne katılıyorum.

useCallbackgeri aramanın, yalnızca girdilerden biri değiştiğinde değişen, hafızaya alınmış bir sürümünü döndürür. Bu, geri aramaları, gereksiz işlemeleri önlemek için referans eşitliğine dayanan optimize edilmiş alt bileşenlere geçirirken yararlıdır (ör. ShouldComponentUpdate).

Misal

Diyelim ki, yalnızca değiştirildikten sonra yeniden render edecek olan PureComponent-based bir çocuğumuz <Pure />var props.

Bu kod, üst öğe her yeniden oluşturulduğunda çocuğu yeniden oluşturur - çünkü satır içi işlev her seferinde referans olarak farklıdır:

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

Bunu şunun yardımıyla halledebiliriz useCallback:

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

Ancak bir kez adeğiştirildiğinde, onPureChangeoluşturduğumuz ve bizim için React'in hatırladığı işleyici işlevinin hala eski adeğeri gösterdiğini görürüz ! Performans sorunu yerine bir hatamız var! Bunun nedeni , bildirildiğinde yakalanan değişkene onPureChangeerişmek için bir kapama kullanmasıdır . Bunu düzeltmek için, React'e nereye bırakılacağını ve doğru verilere işaret eden yeni bir sürümü yeniden yaratacağını / hatırlayacağını (hatırlayacağını) bildirmemiz gerekiyor. Biz ekleyerek bunu bir şekilde bağımlılık useCallback `ikinci argüman:aonPureChangeonPureChangea

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

Şimdi, adeğiştirilirse, React bileşeni yeniden işler. Ve yeniden oluşturma sırasında, bağımlılığın onPureChangefarklı olduğunu görür ve geri aramanın yeni bir sürümünü yeniden oluşturma / hatırlama ihtiyacı vardır. Sonunda her şey çalışıyor!

NB sadece PureComponent/ için değil React.memo, referans eşitlik bir şeyi bağımlılık olarak kullanırken kritik olabilir useEffect.


19

useCallbackVs için tek satırlık useMemo:

useCallback(fn, deps)olduğu eşdeğer etmek useMemo(() => fn, deps).


İle useCallbacksen işlevleri memoize, useMemoherhangi hesaplanmış değeri memoizes:

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

(1)fnaynı olduğu sürece - birden çok işlemede aynı referansın hatırlanmış bir sürümünü döndürür dep. Ama her seferinde sen çağırmak memoFn , karmaşık hesaplama yeniden başlar.

(2)fnher depdeğiştiğinde çağıracak ve döndürülen değerini ( 42burada) hatırlayacak ve daha sonra içinde saklanacaktır memoFnReturn.


18

Nota alınmış geri aramayı her seferinde, şunları yaptığınızda arıyorsunuz:

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

Bu yüzden sayısı useCallbackartıyor. Ancak işlev asla değişmez, hiçbir zaman **** yeni bir geri arama oluşturmaz, her zaman aynıdır. Anlam useCallback, işini doğru yapmaktır.

Bunun doğru olduğunu görmek için kodunuzda bazı değişiklikler yapalım. lastComputedCallbackYeni (farklı) bir işlevin döndürülüp döndürülmediğini takip edecek global bir değişken oluşturalım . Yeni bir işlev döndürülürse, bu useCallbackyalnızca "yeniden çalıştırıldı" anlamına gelir . Bu yüzden tekrar çalıştığında arayacağız expensiveCalc('useCallback'), çünkü useCallbackişe yaradıysa böyle sayarsınız. Bunu aşağıdaki kodda yapıyorum ve artık useCallbackbeklendiği gibi ezberlediği anlaşılıyor .

Her useCallbackseferinde işlevi yeniden oluşturmayı görmek istiyorsanız , geçen dizideki satırın açıklamasını kaldırın second. İşlevi yeniden oluşturduğunu göreceksiniz.

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

Fayda useCallbackyüzden değil tepki iade işlevi aynı olmasıdır removeEventListener'ing ve addEventListenereleman everytime ing, SÜRECE computedCallbackdeğişiklikleri. Ve computedCallbackdeğişkenler değiştiğinde sadece değişir. Böylece tepki yalnızca bir addEventListenerkez olacaktır .

Harika soru, cevaplayarak çok şey öğrendim.


2
iyi yanıta sadece küçük bir yorum: asıl amaç addEventListener/removeEventListener(bu operasyonun kendisi ağır değildir çünkü DOM yeniden akıtmaya / yeniden PureComponentshouldComponentUpdate()
boyamaya yol açmaz

Teşekkürler @skyboyer *EventListenerUcuz olma konusunda hiçbir fikrim yoktu , bu yeniden akışa / boyaya neden olmaması için harika bir nokta! Her zaman pahalı olduğunu düşündüm, bu yüzden kaçınmaya çalıştım. Öyleyse, a'ya geçmediğim durumda PureComponent, karmaşıklık useCallback, tepkiye sahip olma takasına değer remove/addEventListenermi ve DOM ekstra karmaşıklık mı yapıyor?
Noitidart

1
iç içe bileşenler için kullanmazsanız PureComponentveya özel olarak kullanmazsanız , herhangi bir değer katmaz (ikinci bağımsız değişken için fazladan kontrol yapılması, fazladan hareketi atlamayı geçersiz shouldComponentUpdateuseCallbackuseCallbackremoveEventListener/addEventListener
kılar

Vay canına çok ilginç, bunu paylaştığın için teşekkür ederim, bu benim için nasıl *EventListenerpahalı bir operasyon olmadığına dair yepyeni bir bakış .
Noitidart

2

useMemove not alma özelliğini useCallbackkullanın.

Hafızayı bir şeyi hatırlamak olarak düşünmeyi seviyorum .

İkisi de useMemove useCallback hatırla arasında bir şeyler dependancies.Among değişikliğine kadar kılar, fark sadece onların ne hatırlamıyorum .

useMemo niyet unutmayın sizin işlevinden döndürülen değeri.

useCallback niyet hatırlamak gerçek fonksiyonunu.

Kaynak: useMemo ve useCallback arasındaki fark nedir?

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.