useCallback 和 useMemo 在实践中有什么区别?

IT技术 reactjs
2021-04-14 04:34:23

也许我误解了一些东西,但是每次重新渲染发生时 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>

5个回答

TL; 博士;

  • 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 会重新渲染组件。并且在re-render的时候,看到for的依赖onPureChange不同,需要重新创建/记忆新版本的回调。最后一切正常!

NB 不仅仅是对于PureComponent/ React.memo,当使用某些东西作为useEffect.

我认为这条评论并没有很好地将所有内容联系在一起。添加回调和依赖数组是否意味着如果 a 没有改变, <Pure /> 将不会被重新渲染?
2021-05-24 04:34:23
因为onPureChange在这种情况下回调在引用上是相同的,所以是的,<Pure>当它的父级重新渲染时不会重新渲染
2021-06-04 04:34:23
because the inline function is referentially different each time这才是真正让我明白这一点的原因。谢谢!!
2021-06-19 04:34:23

useMemouseCallback使用记忆功能。

我喜欢把记忆看作是记住一些东西

虽然两者useMemouseCallback 记住之间呈现的东西,直到依赖关系的变化,不同的只是它们是什么记忆

useMemo记住函数的返回值。

useCallback记住你的实际功能。

来源:useMemo 和 useCallback 有什么区别?

精彩回答
2021-06-07 04:34:23

一衬里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.

您每次都在调用记忆回调,当您这样做时:

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())使用此回调的子项
2021-06-10 04:34:23
谢谢@skyboyer 我不知道*EventListener便宜,这是一个很好的一点,它不会导致回流/油漆!我一直认为它很贵,所以我尽量避免它。因此,在我没有传递给 a 的情况下PureComponent,是否useCallback值得让 react 和 DOM 做额外的复杂性权衡而增加的复杂性remove/addEventListener
2021-06-11 04:34:23
如果不使用PureComponent或自定义shouldComponentUpdate嵌套组件,则useCallback不会添加任何值(额外检查第二个参数的useCallback开销将使跳过额外removeEventListener/addEventListener移动无效
2021-06-11 04:34:23
哇,太有趣了,谢谢你分享这个,这是一个全新的面貌,对我*EventListener来说不是一项昂贵的手术。
2021-06-21 04:34:23

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

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

本文介绍了不同的 React Memoization 方法: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 用于记忆函数。