【问题标题】:What's the difference between useCallback and useMemo in practice?在实践中 useCallback 和 useMemo 有什么区别?
【发布时间】:2019-07-24 14:02:53
【问题描述】:

也许我误解了什么,但是每次重新渲染发生时 useCallback Hook 都会运行。

我传递了输入 - 作为 useCallback 的第二个参数 - 不可更改的常量 - 但返回的记忆回调仍然在每次渲染时运行我昂贵的计算(我很确定 - 你可以在 sn-p 中自己检查下面)。

我已将 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>

【问题讨论】:

  • 我认为您不需要致电computedCallback = calcCallback();computedCallback 应该只是 = calcCallback, it will update the callback once neverChange` 更改。
  • useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。
  • 每当你用括号声明一个函数时,它都会调用这个函数。因此,每当 second 更新时,它都会执行 calcCallback 函数

标签: reactjs


【解决方案1】:

我们可以使用**useCallback**来记忆一个函数,这意味着这个函数只有在依赖数组中的任何依赖发生变化时才会被重新定义。

**useMemo(() =&gt; computation(a, b), [a, b])** 是让我们记住昂贵计算的钩子。给定相同的 [a, b] 依赖关系,一旦被记忆,钩子将返回记忆值而不调用计算(a,b)。

这篇关于不同 React 记忆方法的文章:React.memo, useMemo,useCallback,它们与实际有什么不同 例子: https://medium.com/geekculture/great-confusion-about-react-memoization-methods-react-memo-usememo-usecallback-a10ebdd3a316

useMemo 用于记忆值,React.memo 用于包装 React Function 组件以防止重新渲染。 useCallback 用于记忆函数。

【讨论】:

    【解决方案2】:

    TL;DR;

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

    加长版:

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

    useCallback 专注于不同的事情:当像onClick={() =&gt; { doSomething(...); } 这样的内联事件处理程序导致PureComponent 子重新渲染时,它修复了性能问题(因为函数表达式每次引用不同)

    这就是说,useCallback 更接近于useRef,而不是一种记忆计算结果的方式。

    查看docs 我确实同意那里看起来很混乱。

    useCallback 将返回回调的记忆版本,仅当其中一个输入发生更改时才会更改。这在将回调传递给依赖引用相等性以防止不必要的渲染的优化子组件时很有用(例如 shouldComponentUpdate)。

    示例

    假设我们有一个基于PureComponent 的子&lt;Pure /&gt;,只有在其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 并重新创建/记住(memoize)指向正确数据的新版本。我们通过在 `useCallback 的第二个参数中添加 a 作为 依赖 来做到这一点:

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

    现在,如果 a 被更改,React 会重新渲染组件。并且在重新渲染期间,它发现onPureChange 的依赖关系不同,需要重新创建/记忆新版本的回调。最后一切正常!

    注意不仅仅是PureComponent/React.memo,在useEffect 中使用某些东西作为依赖项时,引用相等性可能很关键。

    【讨论】:

    • 我认为这条评论并没有很好地将所有内容联系在一起。添加回调和依赖数组是否意味着a没有改变就不会重新渲染
    • 因为在这种情况下onPureChange 回调在引用上是相同的,那么是的,&lt;Pure&gt; 在其父级重新渲染时不会重新渲染
    • because the inline function is referentially different each time 这才是真正让我明白这一点的原因。谢谢!!
    【解决方案3】:

    useMemouseCallback 使用记忆。

    我喜欢将记忆视为记住某事

    虽然useMemouseCallback记住渲染之间的某些内容,直到依赖关系发生变化,但区别只是他们记住

    useMemo记住您的函数的返回值。

    useCallback记住您的实际功能。

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

    【讨论】:

    • 精彩解答
    【解决方案4】:

    useCallbackuseMemo 的单线:

    useCallback(fn, deps)等价于useMemo(() =&gt; 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) 将在每次dep 更改时调用fn 并记住它的返回值(此处为42),然后将其存储在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"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
    <div id="root"></div>
    <script>var { useReducer, useEffect, useState, useRef, useCallback, useMemo } = React</script>

    【讨论】:

      【解决方案5】:

      您每次都在调用 memoized 回调:

      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 的好处是返回的函数是相同的,所以 react 不是每次都对元素进行removeEventListener'ing 和addEventListenering,除非computedCallback 发生变化。而computedCallback 仅在变量更改时才会更改。因此 react 只会addEventListener 一次。

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

      【讨论】:

      • 只是对好的答案的小评论:主要目标不是addEventListener/removeEventListener(这个操作本身并不重,因为不会导致DOM重排/重绘)而是避免重新渲染PureComponent(或使用此回调的自定义 shouldComponentUpdate()) 子级
      • 谢谢@skyboyer 我不知道*EventListener 便宜,这是一个很好的点,它不会导致回流/油漆!我一直认为它很贵,所以我试图避免它。所以在我没有传递给PureComponent的情况下,useCallback 增加的复杂性是否值得用 react 和 DOM 做额外的复杂性remove/addEventListener 进行权衡?
      • 如果不使用PureComponent 或自定义shouldComponentUpdate 用于嵌套组件,那么useCallback 将不会添加任何值(额外检查第二个useCallback 的开销将无效跳过额外@987654350 @移动)
      • 哇,非常有趣,谢谢你分享这个,这是一个全新的面貌,*EventListener 对我来说不是一项昂贵的操作。
      猜你喜欢
      • 2020-06-01
      • 2019-11-18
      • 1970-01-01
      • 1970-01-01
      • 2022-12-18
      • 2022-12-15
      • 2022-12-23
      • 2011-04-03
      相关资源
      最近更新 更多