实际上,useCallback和useMemo有什么区别?

时间:2019-03-02 21:34:34

标签: reactjs

也许我误会了一些东西,但是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>

4 个答案:

答案 0 :(得分:30)

TL; DR;

  • useMemo用来记住函数调用之间以及渲染之间的计算结果
  • useCallback是要记住渲染之间的回调本身(引用相等)
  • useRef用于在渲染器之间保留数据(更新不会触发重新渲染)
  • useState用于在渲染器之间保留数据(更新将触发重新渲染)

长版:

useMemo专注于避免繁重的计算。

useCalback专注于不同的事物:它解决了当onClick={() => { doSomething(...); }之类的内联事件处理程序使PureComponent子级重新呈现时的性能问题(因为函数表达式每次都存在参照差异)

这表示useCallback更接近useRef来记忆计算结果。

PS看着docs,我确实同意它在那里令人困惑。

  

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使用闭包功能来访问变量(而不是通过变量名访问)。为了使这项工作正确进行,我们需要让React知道将onPureChange放到哪里,并重新创建/记住(记忆)指向正确数据的新版本。这里我们需要第二个参数:

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

现在,如果a被更改,React将重新渲染组件(这是useState的举动)。并且在重新渲染期间,它发现onPureChange的输入数据是不同的,并且需要重新创建/存储新版本的回调。终于,一切正常!

答案 1 :(得分:17)

useCallbackuseMemo的单线:

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),然后将其存储在{ {1}}。

memoFnReturn
const App = () => {
  const [dep, setDep] = useState(0);
  const fn = () => 42 + dep; // assuming expensive calculation here
  const memoFn = useCallback(fn, [dep]); // (1)
  const memoFnReturn = useMemo(fn, [dep]); // (2)

  return (
    <div>
      <p> memoFn is {typeof memoFn} </p>
      <p>
        Every call starts new calculation, e.g. {memoFn()} {memoFn()}
      </p>
      <p>memoFnReturn is {memoFnReturn}</p>
      <p>
        Only one calculation for same dep, e.g. {memoFnReturn} {memoFnReturn}
      </p>
      <button onClick={() => setDep((p) => p + 1)}>Change dep</button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

答案 2 :(得分:4)

每次您执行以下操作时,您都会调用记住的回调:

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的好处是返回的函数是相同的,因此每次都不会对元素进行removeEventListeneraddEventListener的反应,除非computedCallback发生变化。并且computedCallback仅在变量更改时更改。因此,反应只会addEventListener一次。

很好的问题,我通过回答中学到了很多东西。

答案 3 :(得分:1)

useMemouseCallback使用备忘录。

我喜欢将回忆记为记住的东西

在渲染之间useMemouseCallback 记住之前,在依赖关系改变之前,区别只是它们记住

useMemo记住函数返回的值。

useCallback记住您的实际功能。

来源:What is the difference between useMemo and useCallback?