実際のuseCallbackとuseMemoの違いは何ですか?


89

何かを誤解したかもしれませんが、再レンダリングが発生するたびにuseCallbackフックが実行されます。

useCallbackの2番目の引数として入力を渡しました-変更できない定数-しかし、返されたメモ化されたコールバックは、すべてのレンダリングで高価な計算を実行します(確かに-以下のスニペットで自分で確認できます)。

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= , it will update the callback once calcCallbackneverChange`の変更である必要があります。
Noitidart

1
useCallback(fn、deps)は、useMemo(()=> fn、deps)と同等です。
ヘンリー・リュー

回答:


155

TL; DR;

  • useMemo 関数の呼び出し間およびレンダリング間で計算結果をメモ化することです
  • useCallback レンダリング間のコールバック自体(参照の同等性)をメモ化することです
  • useRef レンダリング間でデータを保持することです(更新しても再レンダリングは発生しません)
  • useState レンダリング間でデータを保持することです(更新すると再レンダリングが発生します)

ロングバージョン:

useMemo 重い計算を避けることに焦点を当てています。

useCallback別のことに焦点を当てています。インラインイベントハンドラーonClick={() => { doSomething(...); }PureComponent子の再レンダリングを引き起こす場合のパフォーマンスの問題を修正します(関数式は毎回参照的に異なるため)

useCallbackuseRefいえ、計算結果をメモ化する方法というよりは、に近いです。

ドキュメントを調べてみると、混乱しているように見えることに同意します。

useCallback入力の1つが変更された場合にのみ変更される、メモ化されたバージョンのコールバックを返します。これは、不要なレンダリング(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、クロージャを使用して、宣言aonPureChangeにキャプチャされた変数にアクセスするためです。これを修正するonPureChangeには、正しいデータを指す新しいバージョンをドロップして再作成/記憶(メモ化)する場所をReactに通知する必要があります。これを行うには、2番目の引数に依存関係aとして`useCallback:

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

ここで、aが変更された場合、Reactはコンポーネントを再レンダリングします。また、再レンダリング中に、の依存関係onPureChangeが異なることがわかり、新しいバージョンのコールバックを再作成/メモ化する必要があります。最後にすべてが機能します!

NBはないだけのためにPureComponent/ React.memo、参照平等は重要かもしれときで依存関係として使用するものをuseEffect


19

useCallbackvsのワンライナーuseMemo

useCallback(fn, deps)ある同等useMemo(() => fn, deps)


ではuseCallback、あなたが機能をmemoize、useMemo任意の計算値はmemoizes:

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毎回関数を再作成することを確認したい場合は、を渡す配列内の行のコメントを解除します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返された関数はそうではありません反応し、同じであるということであるremoveEventListenerINGのと」addEventListenerがない限り、要素毎回上INGのcomputedCallback変化。そして、computedCallback変数が変化したときにのみ変化します。したがって、反応は一addEventListener度だけです。

素晴らしい質問です。答えることで多くのことを学びました。


2
良い答えへのほんの小さなコメント:主な目標はaddEventListener/removeEventListener(DOMリフロー/PureComponentshouldComponentUpdate()
リペイントに

@skyboyerに感謝します。安いとは思い*EventListenerもしませんでした。リフロー/ペイントが発生しないのは素晴らしい点です。いつも高いと思っていたので避けようとしました。それで、私がに渡していない場合PureComponentuseCallbackreactとDOMに余分な複雑さを持たせることのトレードオフの価値によって複雑さが追加されremove/addEventListenerますか?
Noitidart

1
使用しない場合はPureComponent、カスタムまたはshouldComponentUpdateネストされたコンポーネントのためにuseCallback(オーバーヘッド秒のための余分なチェックによって、任意の値を追加しませんuseCallback、余分なスキップ無効になりますargumgentremoveEventListener/addEventListener移動)
skyboyer

これを共有してくれて本当に興味深いです、それ*EventListenerは私にとってどのように高価な操作ではないかについてのまったく新しい見方です。
Noitidart

2

useMemouseCallbackメモ化を使用します。

はメモ化を何かを覚えていると考えるのが好きです

両方とも、依存関係が変わるまでレンダリング間の何かuseMemouseCallback 覚えていますが、違いは彼らが覚えていることです。

useMemo関数からの戻り値を記憶します。

useCallbackあなたの実際の機能を覚えています。

出典:useMemoとuseCallbackの違いは何ですか?

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.