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


85

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

1
useCallback(fn,deps)等同于useMemo(()=> fn,deps)。
刘Liu

Answers:


148

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并重新创建/记住(记忆)指向正确数据的新版本。为此,我们在ʻuseCallback的第二个参数中添加a了一个依赖项:

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

现在,如果a已更改,React将重新渲染组件。在重新渲染期间,它会发现的依赖关系onPureChange有所不同,因此需要重新创建/存储新版本的回调。终于一切正常!


3
非常详细且<纯>的答案,非常感谢。;)
RegarBoy

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作“ing和addEventListener荷兰国际集团在元件每次,除非computedCallback改变。并且computedCallback唯一的改变是在变量改变时。因此反应只会发生addEventListener一次。

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


2
只是小到很好的答案评论:主要目的不是addEventListener/removeEventListener(这个运算本身不重,因为不会导致DOM回流/重画),但要避免再次渲染PureComponent(或定制shouldComponentUpdate())的孩子,使用此回调
skyboyer

谢谢@skyboyer,我不知道*EventListener价格便宜,这很重要,因为它不会引起回流/油漆!我一直以为它很贵,所以我尽量避免它。因此,在我不采用a的情况下PureComponent,是否useCallback值得通过做出反应和DOM进行权衡来增加复杂性remove/addEventListener
Noitidart '19

1
如果不对嵌套组件使用PureComponent或自定义shouldComponentUpdate,则useCallback不会添加任何值(通过额外检查第二个useCallback累积开销将使跳过多余的removeEventListener/addEventListener动作无效)
skyboyer

哇,非常有趣,谢谢您的分享,这是一个全新的面貌,说明*EventListener对我而言这不是一个昂贵的手术。
Noitidart '19

15

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.