Sự khác biệt giữa useCallback và useMemo trong thực tế là gì?


85

Có thể tôi đã hiểu nhầm điều gì đó, nhưng useCallback Hook luôn chạy khi kết xuất lại xảy ra.

Tôi đã chuyển đầu vào - làm đối số thứ hai cho useCallback - hằng số không thể thay đổi - nhưng lệnh gọi lại được ghi nhớ trả về vẫn chạy các tính toán đắt tiền của tôi ở mỗi lần hiển thị (tôi khá chắc chắn - bạn có thể tự kiểm tra trong đoạn mã bên dưới).

Tôi đã thay đổi useCallback thành useMemo - và useMemo hoạt động như mong đợi - chạy khi các đầu vào được truyền thay đổi. Và thực sự ghi nhớ các phép tính tốn kém.

Ví dụ trực tiếp:

'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
Tôi không nghĩ bạn cần phải gọi computedCallback = calcCallback();. computedCallbackchỉ nên là = calcCallback , it will update the callback once neverChange` thay đổi.
Noitidart

1
useCallback (fn, deps) tương đương với useMemo (() => fn, deps).
Henry Liu

Câu trả lời:


148

TL; DR;

  • useMemo là ghi nhớ kết quả tính toán giữa các lần gọi hàm và giữa các lần hiển thị
  • useCallback là ghi nhớ chính một cuộc gọi lại (bình đẳng tham chiếu) giữa các lần hiển thị
  • useRef là giữ dữ liệu giữa các lần hiển thị (cập nhật không kích hoạt hiển thị lại)
  • useState là giữ dữ liệu giữa các lần hiển thị (cập nhật sẽ kích hoạt kết xuất)

Phiên bản dài:

useMemo chú trọng tránh nặng về tính toán.

useCallbacktập trung vào một thứ khác: nó khắc phục các vấn đề về hiệu suất khi các trình xử lý sự kiện nội tuyến như kết xuất con onClick={() => { doSomething(...); }gây ra PureComponent(bởi vì các biểu thức hàm luôn khác nhau về mặt tham chiếu)

Điều này nói rằng, useCallbackgần với useRef, hơn là một cách để ghi nhớ một kết quả tính toán.

Nhìn vào các tài liệu, tôi đồng ý rằng nó có vẻ khó hiểu ở đó.

useCallbacksẽ trả về phiên bản được ghi nhớ của lệnh gọi lại chỉ thay đổi nếu một trong các đầu vào đã thay đổi. Điều này hữu ích khi chuyển các lệnh gọi lại đến các thành phần con được tối ưu hóa dựa trên bình đẳng tham chiếu để ngăn các kết xuất không cần thiết (ví dụ: shouldComponentUpdate).

Thí dụ

Giả sử chúng ta có một PureComponentcon dựa trên <Pure />nó sẽ chỉ hiển thị lại khi nó propsđược thay đổi.

Mã này hiển thị lại phần tử con mỗi khi phần tử cha được hiển thị lại - bởi vì hàm nội tuyến mỗi lần khác nhau về mặt tham chiếu:

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

Chúng tôi có thể xử lý điều đó với sự trợ giúp của useCallback:

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

Nhưng sau khi ađược thay đổi, chúng tôi thấy rằng onPureChangehàm xử lý mà chúng tôi đã tạo - và React đã ghi nhớ cho chúng tôi - vẫn trỏ về agiá trị cũ ! Chúng tôi đã gặp lỗi thay vì vấn đề về hiệu suất! Điều này là do onPureChangesử dụng một bao đóng để truy cập vào abiến, biến được bắt khi onPureChangeđược khai báo. Để khắc phục điều này, chúng ta cần cho React biết nơi cần thả onPureChangevà tạo lại / ghi nhớ (ghi nhớ) một phiên bản mới trỏ đến dữ liệu chính xác. Chúng tôi làm như vậy bằng cách thêm alàm phụ thuộc trong đối số thứ hai vào `useCallback:

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

Bây giờ, nếu ađược thay đổi, React sẽ hiển thị lại thành phần. Và trong quá trình kết xuất lại, nó thấy rằng sự phụ thuộc cho onPureChangelà khác nhau, và cần phải tạo lại / ghi nhớ một phiên bản mới của lệnh gọi lại. Cuối cùng thì mọi thứ cũng hoạt động!


3
Câu trả lời rất chi tiết và <Pure>, cảm ơn rất nhiều. ;)
RegarBoy

17

Bạn luôn gọi cuộc gọi lại đã ghi nhớ, khi bạn thực hiện:

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

Đây là lý do tại sao số lượng useCallbackđang tăng lên. Tuy nhiên, hàm không bao giờ thay đổi, nó không bao giờ ***** tạo **** một lệnh gọi lại mới, nó luôn như vậy. Có nghĩa useCallbacklà thực hiện chính xác công việc của nó.

Hãy thực hiện một số thay đổi trong mã của bạn để xem điều này là đúng. Hãy tạo một biến toàn cục, biến lastComputedCallbacknày sẽ theo dõi xem một hàm mới (khác) có được trả về hay không. Nếu một hàm mới được trả về, điều đó có nghĩa là useCallbackchỉ được "thực thi lại". Vì vậy, khi nó thực thi một lần nữa, chúng tôi sẽ gọi expensiveCalc('useCallback'), vì đây là cách bạn đếm xem useCallbackcó hoạt động hay không. Tôi thực hiện điều này trong đoạn mã dưới đây và rõ ràng là nó useCallbackđang ghi nhớ như mong đợi.

Nếu bạn muốn thấy useCallbackhàm tạo lại mọi lúc, hãy bỏ ghi chú dòng trong mảng đi qua second. Bạn sẽ thấy nó tạo lại chức năng.

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

Lợi ích useCallbacklà hàm được trả về giống nhau, do đó, phản ứng khôngremoveEventListener cũng ăn addEventListenervào phần tử, BỎ LỠ các computedCallbackthay đổi. Và computedCallbackchỉ thay đổi khi các biến thay đổi. Do đó, phản ứng sẽ chỉ addEventListenermột lần.

Câu hỏi tuyệt vời, tôi đã học được rất nhiều bằng cách trả lời nó.


2
chỉ cần nhận xét nhỏ cho câu trả lời hay: mục tiêu chính không phải về addEventListener/removeEventListener(bản thân op này không nặng vì không dẫn đến việc chỉnh sửa lại DOM / sơn lại) mà là để tránh hiển thị lại PureComponent(hoặc với tùy chỉnh shouldComponentUpdate()) con sử dụng lệnh gọi lại này
skyboyer

Cảm ơn @skyboyer Tôi không có ý tưởng về *EventListenerviệc rẻ, đó là một điểm tuyệt vời về việc nó không gây ra hiện tượng chảy lại / sơn! Tôi luôn nghĩ rằng nó đắt tiền nên tôi cố gắng tránh nó. Vì vậy, trong trường hợp tôi không chuyển đến a PureComponent, liệu độ phức tạp được thêm vào có useCallbackđáng để đánh đổi khi có phản ứng và DOM làm thêm phức tạp remove/addEventListenerkhông?
Noitidart

1
nếu không sử dụng PureComponenthoặc tùy chỉnh shouldComponentUpdatecho các thành phần lồng nhau thì useCallbacksẽ không thêm bất kỳ giá trị (overhead bởi kiểm tra thêm cho thứ hai useCallbackargumgent sẽ vô hiệu hóa bỏ qua thêm removeEventListener/addEventListenermove)
skyboyer

Wow thật thú vị, cảm ơn bạn đã chia sẻ điều này, đó là một cái nhìn hoàn toàn mới về cách thức *EventListenerkhông phải là một ca phẫu thuật tốn kém đối với tôi.
Noitidart

15

Một lớp lót cho useCallbackvs useMemo:

useCallback(fn, deps)tương đương với useMemo(() => fn, deps).


Với các useCallbackhàm ghi nhớ của bạn, useMemoghi nhớ mọi giá trị được tính toán:

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

(1)sẽ trả về một phiên bản đã ghi nhớ của fn- cùng một tham chiếu trên nhiều lần hiển thị, miễn deplà giống nhau. Nhưng mỗi khi bạn gọi memoFn , quá trình tính toán phức tạp đó lại bắt đầu.

(2)sẽ gọi fnmỗi khi depthay đổi và ghi nhớ giá trị trả về của nó ( 42ở đây), sau đó được lưu trữ trong memoFnReturn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.