व्यवहार में UseCallback और useMemo के बीच क्या अंतर है?


85

हो सकता है कि मुझे कुछ गलत लगा हो, लेकिन जब दोबारा रेंडर होता है, तो कॉलबैक हुक का इस्तेमाल हर बार होता है।

मैंने इनपुट्स को पास कर दिया - एक दूसरे तर्क के रूप में उपयोग करने के लिए कॉलबैक - गैर-कभी-परिवर्तनशील स्थिरांक - लेकिन फिर से याद किया गया कॉलबैक अभी भी हर रेंडर पर मेरी महंगी गणना चलाता है (मुझे पूरा यकीन है - आप नीचे स्निपेट में अपने आप से जांच कर सकते हैं)।

मैंने यूज़ बदल दिया है। का उपयोग करने के लिए कॉलबैक - और उपयोग करें मीमो काम करता है - जब इनपुट्स बदल जाता है तो अपेक्षित रूप से चलता है और वास्तव में महंगी गणना को याद करता है।

लाइव उदाहरण:

'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) उपयोग के बराबर है।
हेनरी लियू

जवाबों:


148

टी एल; डॉ;

  • 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हैंडलर फ़ंक्शन बनाया है - और रिएक्ट हमारे लिए याद किया गया - अभी भी पुराने aमूल्य की ओर इशारा करता है! हमें प्रदर्शन समस्या के बजाय बग मिल गया है! ऐसा इसलिए है क्योंकि चर onPureChangeका उपयोग करने के लिए एक बंद का उपयोग करता aहै, जिसे onPureChangeघोषित किए जाने पर कब्जा कर लिया गया था। इसे ठीक करने के लिए हमें रिएक्ट को यह बताने की जरूरत है onPureChangeकि एक नया संस्करण कहां छोड़ना / फिर से बनाना / याद रखना (याद रखना) जो सही डेटा की ओर इशारा करता है। हम 'useCallback' के दूसरे तर्क में aएक निर्भरता के रूप में जोड़कर ऐसा करते हैं :

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

अब, यदि aबदला गया है, तो रिएक्ट घटक को फिर से प्रस्तुत करता है। और फिर से प्रस्तुत करने के दौरान, यह देखता है कि निर्भरता onPureChangeअलग है, और कॉलबैक के एक नए संस्करण को फिर से बनाने / याद रखने की आवश्यकता है। अंत में सब कुछ काम करता है!


3
बहुत विस्तृत और <शुद्ध> उत्तर, बहुत बहुत धन्यवाद। ;)
रेगबॉय

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' addEventListenerतत्व पर आईएनजी और आईएनजी हर समय नहीं होती है, computedCallbackपरिवर्तन को कम करें। और computedCallbackकेवल तब बदलता है जब चर बदलते हैं। इस प्रकार केवल addEventListenerएक बार प्रतिक्रिया होगी ।

महान प्रश्न, मैंने इसका उत्तर देकर बहुत कुछ सीखा।


2
अच्छा जवाब देने के लिए बस छोटी सी टिप्पणी: मुख्य लक्ष्य के बारे में addEventListener/removeEventListener(यह ऑप अपने आप में भारी नहीं है क्योंकि डोम PureComponentshouldComponentUpdate()
रिफ्लो

धन्यवाद @skyboyer मुझे *EventListenerसस्ते होने के बारे में कोई पता नहीं था , इसके बारे में एक महान बात यह है कि यह रिफ्लो / पेंट का कारण नहीं है! मुझे हमेशा लगता था कि यह महंगा है इसलिए मैंने इससे बचने की कोशिश की। तो जिस मामले में मैं पास नहीं हो रहा हूं PureComponent, useCallbackक्या प्रतिक्रिया और डीओएम अतिरिक्त जटिलता करते हैं, क्या जटिलता को व्यापार से जोड़ा जाता है remove/addEventListener?
Noitidart

1
यदि नेस्टेड घटकों के लिए उपयोग PureComponentया कस्टम नहीं है, shouldComponentUpdateतो useCallbackकोई मूल्य नहीं जोड़ेंगे (दूसरे useCallbackargumgent के लिए अतिरिक्त जाँच द्वारा ओवरहेड अतिरिक्त removeEventListener/addEventListenerकदम लंघन को कम कर देगा )
Skyboyer

वाह सुपर दिलचस्प यह साझा करने के लिए धन्यवाद, यह एक नया रूप है कि *EventListenerमेरे लिए एक महंगा ऑपरेशन कैसे नहीं है।
Noitidart

15

एक लाइनर useCallbackबनाम के लिए useMemo:

useCallback(fn, deps)के बराबर है useMemo(() => fn, deps)


साथ useCallbackआप कार्यों memoize, useMemomemoizes किसी भी गणना मूल्य:

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 Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.