ความแตกต่างระหว่าง useCallback และ useMemo ในทางปฏิบัติคืออะไร?


89

บางทีฉันอาจเข้าใจผิดบางอย่าง แต่ 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)
Henry Liu

คำตอบ:


155

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แตกต่างกันและมีความจำเป็นที่จะต้องสร้าง / บันทึกการโทรกลับเวอร์ชันใหม่อีกครั้ง ในที่สุดทุกอย่างก็ใช้ได้!

หมายเหตุไม่เพียง แต่สำหรับPureComponent/ React.memoเท่านั้นความเท่าเทียมกันในการอ้างอิงอาจมีความสำคัญเมื่อใช้บางสิ่งเป็นการพึ่งพาในuseEffect.


19

ซับเดียวสำหรับuseCallbackvs 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ซึ่งถูกเก็บไว้แล้วใน


18

คุณกำลังโทรติดต่อกลับที่บันทึกไว้ทุกครั้งเมื่อคุณทำ:

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

นี่คือสาเหตุที่จำนวนuseCallbackเพิ่มขึ้น อย่างไรก็ตามฟังก์ชั่นไม่เคยเปลี่ยนแปลงมันไม่เคย ***** สร้าง **** การโทรกลับใหม่มันเหมือนเดิมเสมอ ความหมายuseCallbackคือการทำงานอย่างถูกต้อง

มาทำการเปลี่ยนแปลงบางอย่างในโค้ดของคุณเพื่อดูว่านี่เป็นจริง มาสร้างตัวแปรโกลบอลlastComputedCallbackซึ่งจะติดตามว่ามีการส่งคืนฟังก์ชันใหม่ (อื่น) หรือไม่ หากฟังก์ชันใหม่ถูกส่งกลับนั่นหมายความว่าuseCallbackเพียงแค่ "ดำเนินการอีกครั้ง" ดังนั้นเมื่อดำเนินการอีกครั้งเราจะเรียกexpensiveCalc('useCallback')ว่านี่คือวิธีที่คุณนับว่าuseCallbackได้ผลหรือไม่ ฉันทำสิ่งนี้ในรหัสด้านล่างและตอนนี้ชัดเจนแล้วว่าuseCallbackกำลังบันทึกตามที่คาดไว้

หากคุณต้องการที่จะเห็นuseCallbackสร้างใหม่อีกครั้งทุกฟังก์ชั่นแล้ว uncomment 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คือฟังก์ชันที่ส่งคืนจะเหมือนกันดังนั้น react จึงไม่ใช่removeEventListener'ing and addEventListenering on the element ทุกครั้ง, UNLESS the computedCallbackchanges. และสิ่งcomputedCallbackเดียวที่เปลี่ยนแปลงเมื่อตัวแปรเปลี่ยนไป ดังนั้นปฏิกิริยาจะเกิดขึ้นเพียงaddEventListenerครั้งเดียว

คำถามที่ดีฉันได้เรียนรู้มากมายจากการตอบคำถามนี้


2
ความคิดเห็นเล็ก ๆ น้อย ๆ สำหรับคำตอบที่ดี: เป้าหมายหลักไม่เกี่ยวกับaddEventListener/removeEventListener(ตัวปฏิบัติการนี้ไม่หนักเนื่องจากไม่ได้นำไปสู่การ reflow / ทาสีใหม่ของ DOM) แต่เพื่อหลีกเลี่ยงการเรนเดอร์PureComponent(หรือด้วยการกำหนดเองshouldComponentUpdate()) เด็กที่ใช้การโทรกลับนี้
skyboyer

ขอบคุณ @skyboyer ฉันไม่รู้เลยว่า*EventListenerราคาถูกนั่นเป็นจุดที่ดีเกี่ยวกับการไม่ทำให้เกิด reflow / paint! ฉันคิดเสมอว่ามันแพงดังนั้นฉันจึงพยายามหลีกเลี่ยง ดังนั้นในกรณีที่ฉันไม่ได้ผ่านไปยัง a PureComponentความซับซ้อนที่เพิ่มเข้ามาโดยuseCallbackคุ้มค่ากับการแลกเปลี่ยนจากการตอบสนองและ DOM มีความซับซ้อนเป็นพิเศษremove/addEventListenerหรือไม่?
Noitidart

1
หากไม่ใช้PureComponentหรือกำหนดเองshouldComponentUpdateสำหรับส่วนประกอบที่ซ้อนกันuseCallbackจะไม่เพิ่มค่าใด ๆ (ค่าใช้จ่ายโดยการตรวจสอบเพิ่มเติมสำหรับuseCallbackอาร์กิวเมนต์ที่สองจะทำให้การข้ามการremoveEventListener/addEventListenerเคลื่อนไหวพิเศษเป็นโมฆะ)
skyboyer

ว้าวน่าสนใจสุด ๆ ขอบคุณสำหรับการแบ่งปันสิ่งนี้เป็นรูปลักษณ์ใหม่ทั้งหมดว่า*EventListenerการดำเนินการที่ไม่แพงสำหรับฉัน
Noitidart

2

useMemoและuseCallbackใช้การบันทึก

ผมชอบที่จะคิดว่า memoization เป็นความทรงจำบางสิ่งบางอย่าง

ขณะที่ทั้งสองuseMemoและuseCallback จำไว้ว่าบางสิ่งบางอย่างระหว่างวาทกรรมจนกว่าจะมีการเปลี่ยนแปลง dependancies แตกต่างกันคือสิ่งที่พวกเขาจำได้

useMemoจะจดจำค่าที่ส่งคืนจากฟังก์ชันของคุณ

useCallbackจะจดจำฟังก์ชันที่แท้จริงของคุณ

ที่มา: useMemo และ useCallback ต่างกันอย่างไร?

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.