React中的useCallback和useMemo有什么作用?

83

如同文档所述,useCallback返回一个记忆化的回调函数。

传入一个内联回调函数和一个输入数组,useCallback会返回一个记忆化版本的回调函数,仅在输入值发生变化时才会改变。当向需要参考相等性以防止不必要的渲染(例如shouldComponentUpdate)的优化子组件传递回调函数时,这非常有用。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

但是它是如何工作的,在React中最好的使用场景是哪里?

P.S. 我认为通过codepen示例进行可视化会帮助每个人更好地理解。 在文档中解释

5个回答

147

当您希望通过优化性能来避免不必要的重新渲染时,最好使用此方法。

比较以下两种传递回调给子组件的方式,取自React文档:

1. 在Render中使用箭头函数

class Foo extends Component {
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <Button onClick={() => this.handleClick()}>Click Me</Button>;
  }
}

2. 构造函数中的绑定(ES2015)

class Foo extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log('Click happened');
  }
  render() {
    return <Button onClick={this.handleClick}>Click Me</Button>;
  }
}

假设 <Button> 被实现为一个 PureComponent,第一种方式会导致每次 <Foo> 重新渲染时 <Button> 都会重新渲染,因为在每个 render() 调用中都会创建一个新的函数。而在第二种方式中,handleClick 方法只会在 <Foo> 的构造函数中创建一次并在不同的渲染中重复使用。

如果我们将这两种方法转换为使用 hooks 的函数组件,它们的等价方式如下:

1. Render 中的箭头函数 -> 未进行 memoization 的回调函数

function Foo() {
  const handleClick = () => {
    console.log('Click happened');
  }
  return <Button onClick={handleClick}>Click Me</Button>;
}

2. 构造函数绑定 (ES2015) -> 记忆化回调函数

function Foo() {
  const memoizedHandleClick = useCallback(
    () => console.log('Click happened'), [],
  ); // Tells React to memoize regardless of arguments.
  return <Button onClick={memoizedHandleClick}>Click Me</Button>;
}

第一种方式在每次函数组件调用时创建回调,但是在第二种方式中,React为您记忆了回调函数,因此回调不会被多次创建。

因此,如果使用React.memo实现


2
那么,如果我的handleClick函数需要进行一些数据获取或参数更改,那我就不应该使用useCallback,是吗? - ZiiMakc
2
如果您的返回值即使使用相同的参数也可能发生更改,那么您不应该使用useCallback,因为返回值是记忆化的。 - Yangshun Tay
@YangshunTay 请记住,如果您将参数传递到useCallback函数中并在末尾添加[](以避免跟踪更改),它将始终返回记忆化函数而不更新参数。 - Jony-Y
示例需要包括使用事件对象的示例。 - user239558
对于那些没有注意到第二个参数的人,可能会好奇为什么它不起作用。实际上,需要将useCallback的第二个参数设置为[],以便告诉React无论参数如何都要进行记忆化处理。 - Supasate
显示剩余2条评论

21

useCallbackuseMemo 是 React hooks 的功能,旨在绕过函数式编程方法的弱点。在JavaScript中,无论是函数、变量还是任何其他实体,在执行时都会被创建到内存中。这对于React来说是个大问题,因为它需要检测组件是否需要重新渲染。重新渲染的需求是根据输入道具和上下文推断出来的。让我们看一个没有使用useCallback的简单例子。

const Component = () => {
  const [counter, setCounter] = useState(0);

  const handleClick = () => {
    setCounter(counter + 1);
  }

  return <div>
    Counter:{counter}<br/>
    <button onClick={handleClick}>+1</button>
  </div>
}
请注意,handleClick函数实例将在块内的每个函数调用上创建,因此每次调用事件处理程序的地址都会不同。由于此原因,React框架始终会视事件处理程序为已更改。在上面的示例中,React将在每个调用上将handleClick视为新值。它简单地没有工具来将其识别为相同的调用。 useCallback的作用是在未更改所列变量的情况下,内部存储该函数的第一个引入版本并将其返回给调用者。
const Component = () => {
  const [counter, setCounter] = useState(0);

  const handleClick = useCallback(() => {
    setCounter(counter + 1);
  }, [])

  return <div>
    Counter:{counter}<br/>
    <button onClick={handleClick}>+1</button>
  </div>
}

现在,通过上面的代码,React 将识别 handleClick -事件处理程序为相同,这要归功于 useCallback 函数调用。它将始终返回函数的相同实例,并使 React 组件渲染机制保持顺畅。

通过 useCallback 内部存储函数将导致新问题。存储的函数调用实例将无法直接访问当前函数调用的变量。相反,它将看到在创建存储函数的初始闭包调用中引入的变量。因此,对于更新的变量,该调用将不起作用。这就是为什么需要告诉 useCallback 某些使用的变量是否已更改。因此,useCallback 将把当前函数调用实例存储为新的存储实例。第二个参数作为 useCallback 的变量列表列出了此功能的变量。在我们的示例中,我们需要告诉 useCallback 函数,我们需要在每次调用时使用一个新的 counter 变量版本。如果不这样做,调用后计数器值将始终为 1,这是从原始值 0 加 1 而来。

const Component = () => {
  const [counter, setCounter] = useState(0);

  const handleClick = useCallback(() => {
    setCounter(counter + 1);
  }, [counter])

  return <div>
    Counter:{counter}<br/>
    <button onClick={handleClick}>+1</button>
  </div>
}

现在我们有一个可工作的代码版本,不会在每次调用时重新渲染。

需要注意的是,useState 调用也是出于同样的原因。函数块没有内部状态,所以钩子使用 useStateuseCallbackuseMemo 来模拟类的基本功能。从这个意义上讲,函数式编程是向过程式编程更接近的一个大步骤回归到历史中去了。

useMemo 是与 useCallback 相同类型的机制,但用于其他对象和变量。通过它,您可以限制组件重新呈现的需求,因为如果列出的字段未更改,则 useMemo 函数将在每次函数调用时返回相同的值。

这种新的 React 钩子方法中的一部分无疑是系统最薄弱的地方。useCallback 相当令人费解,而且容易出错。使用 useCallback 调用和依赖项时,很容易陷入内部循环。这一点在 React Class 方法中是不存在的。

毕竟,使用类的原始方法更有效率。 useCallback 会减少重新呈现的需求,但它会在某些相关变量发生更改时再次生成函数,并匹配变量是否已更改本身会产生开销。这可能导致不必要的多次重新呈现。而在 React 类中就不存在这种情况。


1
这是一个很棒的答案! - Saber
太棒了。你应该开始写博客或其他东西。 - Mateusz
很棒的回答。最近我真的感受到了这种痛苦,因为被不必要地要求将一个React Native代码库从React类迁移走。 - ksav
根据这个答案,每次组件重新渲染时,该函数仍然会被重新创建。 - ospider
@ospider,在每次调用时自然地创建函数。这正是为什么需要这种解决方法的原因。React的use-functions使得旧的函数被使用,而不是新创建的函数。这样,React就可以避免不必要的重新渲染。 - Ville Venäläinen

8

我制作了一个小例子,以帮助其他人更好地理解它的行为。您可以在此处运行演示(链接)或阅读下面的代码:

import React, { useState, useCallback, useMemo } from 'react';
import { render } from 'react-dom';

const App = () => {
    const [state, changeState] = useState({});
    const memoizedValue = useMemo(() => Math.random(), []);
    const memoizedCallback = useCallback(() => console.log(memoizedValue), []);
    const unMemoizedCallback = () => console.log(memoizedValue);
    const {prevMemoizedCallback, prevUnMemoizedCallback} = state;
    return (
      <>
        <p>Memoized value: {memoizedValue}</p>
        <p>New update {Math.random()}</p>
        <p>is prevMemoizedCallback === to memoizedCallback: { String(prevMemoizedCallback === memoizedCallback)}</p>
        <p>is prevUnMemoizedCallback === to unMemoizedCallback: { String(prevUnMemoizedCallback === unMemoizedCallback) }</p>
        <p><button onClick={memoizedCallback}>memoizedCallback</button></p>
        <p><button onClick={unMemoizedCallback}>unMemoizedCallback</button></p>
        <p><button onClick={() => changeState({ prevMemoizedCallback: memoizedCallback, prevUnMemoizedCallback: unMemoizedCallback })}>update State</button></p>
      </>
    );
};

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

我不确定这是否有用,memoizedCallback无效,因为它访问了memoizedValue,并且deps列表为空,是吧? - user239558
1
@user239558 当你按下“更新状态”按钮更新状态时,你应该看到prevMemoizedCallback等于next memoizedCallback,这样做的目的是为了记忆化一个回调函数,可以在更新中使用。在这个例子中,我使用memoizedValue只是为了演示你可以利用初始渲染生成的第一个随机值以及其他更新,每次都会生成新的随机值。 - stackoverflow

0
默认情况下,每次渲染时事件处理程序都会被重新创建并分配不同的地址,从而导致更改了“props”对象。在下面的示例中,按钮 2 不会被重复渲染,因为“props”对象没有发生变化。请注意,在每次渲染时整个 Example() 函数都会运行到完成。
const MyButton = React.memo(props=>{
   console.log('firing from '+props.id);
   return (<button onClick={props.eh}>{props.id}</button>);
});

function Example(){
   const [a,setA] = React.useState(0);
   const unmemoizedCallback = () => {};
   const memoizedCallback = React.useCallback(()=>{},[]);   // don’t forget []!
   setTimeout(()=>{setA(a=>(a+1));},3000);
   return (<React.Fragment>
                 <MyButton id="1" eh={unmemoizedCallback}/>
                 <MyButton id="2" eh={memoizedCallback}/>
                 <MyButton id="3" eh={()=>memoizedCallback}/>
           </React.Fragment>);
} 
ReactDOM.render(<Example/>,document.querySelector("div"));

0

useCallback → 如果在父组件中渲染了多个子组件,并且将事件处理程序作为props传递给子组件。如果父组件中的状态发生更新,即使子组件不依赖于被更新的状态,也会重新渲染父组件。这会导致额外的重新渲染,从而影响性能。

为了解决这些问题,我们可以使用useCallback钩子来缓存函数本身,因为每次组件渲染都会创建新的函数实例。

在下面的示例中,每次切换按钮的点击事件时,计数器组件都会重新渲染,因此我们使用回调钩子来阻止重新渲染。

    import React, {useState, useEffect, useCallback} from 'react';
    
    const IncrementCounter = (props) => {
        useEffect(() => {
            console.log('Counter Function Called');
        }, [props.incrementVal]);
        return (
            <div>
                <button onClick={() => props.incrementVal(1)}>Increment Counter</button>
                <p>Counter Val: {props.counter}</p>
            </div>
        )
    }
    
    function Question2CallBack(props) {
        const [toggle, setToggle] = useState(false);
        const [counter, setCounter] = useState(0);
    
        // USING USECALLBACK FOR CATCHING THE SAME REFRENCE OF FUNCTION WHILE EACH RENDER DURING STATE CHANGE
        const incrementVal = useCallback((val) => {
            setCounter(()=> counter + val);
        }, [counter]);
    
        return (
            <div>
                <button onClick={() => setToggle(!toggle)}>Toggle</button>
                <p>Toggle Value: {toggle ? 'true': 'false'}</p>
                <IncrementCounter incrementVal={incrementVal} counter={counter}/>
            </div>
        );
    }
    
    export default Question2CallBack;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接