80

正如docs中所说,useCallback 返回一个记忆化的回调。

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

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

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

PS我认为使用codepen示例进行可视化将帮助每个人更好地理解它。在文档中解释

4

4 回答 4

142

当您想要防止不必要的重新渲染以获得更好的性能时,最好使用此选项。

比较这两种将回调传递给从React Docs获取的子组件的方式:

一、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>实现为 a PureComponent,第一种方式将导致<Button>每次重新渲染时都重新渲染,因为每次调用<Foo>都会创建一个新函数。render()第二种方式,该handleClick方法仅在<Foo>的构造函数中创建一次,并在渲染之间重用。

如果我们使用钩子将这两种方法转换为功能组件,它们是等价的(有点):

1. Render 中的箭头函数 -> Un-memoized 回调

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

2. Bind in Constructor (ES2015) -> Memoized callbacks

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

第一种方式在函数组件的每次调用时都会创建回调,但第二种方式,React 会为你记住回调函数,并且不会多次创建回调。

因此,在第一种情况下,如果Button使用React.memo它实现总是会重新渲染(除非您有一些自定义比较功能),因为onClick道具每次都不同,在第二种情况下,它不会。

在大多数情况下,第一种方式是可以的。正如 React 文档所述:

在渲染方法中使用箭头函数可以吗?一般来说,是的,它是可以的,而且通常是最简单的向回调函数传递参数的方法。

如果您确实有性能问题,请务必优化!

于 2018-11-06T18:25:47.657 回答
11

useCallbackuseMemo试图绕过使用 React 钩子选择的函数式编程方法所带来的弱点。在 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-event 处理程序,这要归功于 -functionuseCallback调用。它将始终返回相同的函数实例,并且 React 组件渲染机制会很高兴。

将函数存储在内部useCallback会带来一个新问题。函数调用的存储实例将无法直接访问当前函数调用的变量。相反,它将看到在创建存储函数的初始闭包调用中引入的变量。因此,该调用不适用于更新的变量。这就是为什么你需要知道一些使用的变量是否已经改变。这样 useCallback 会将当前的函数调用实例存储为一个新的存储实例。作为第二个参数的变量useCallback列表列出了此功能的变量。在我们的示例中,我们需要告诉useCallback-函数,我们需要在每次调用时都有一个新版本的 counter -variable。如果我们不这样做,调用后的计数器值将始终为 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-call 出于同样的原因在这里。功能块没有内部状态,所以钩子使用useState,useCallbackuseMemo模仿类的基本功能。从这个意义上说,函数式编程是历史上更接近过程式编程的一大步。

useMemo与其他对象和变量的机制相同useCallback。使用它,您可以限制对组件重新渲染的需求,因为useMemo如果列出的字段没有更改,-function 将在每个函数调用上返回相同的值。

这部分新的 React hooks 方法绝对是系统的最薄弱环节。useCallback非常违反直觉并且非常容易出错。使用useCallback-calls 和依赖关系,很容易最终追逐内部循环。我们在 React Class 方法中没有这个警告。

毕竟,最初的类方法更有效。这useCallback将减少重新渲染的需要,但每次它的某些因变量发生变化时,它都会重新生成函数,如果变量本身发生变化,匹配就会产生开销。这可能会导致不必要的重新渲染。React 类不是这种情况。

于 2021-04-11T09:36:39.330 回答
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'));
于 2019-04-08T19:16:44.707 回答
0

默认情况下,每次渲染都会重新创建事件处理程序并为其分配不同的地址,从而导致“道具”对象发生变化。下面,按钮 2 没有重复渲染,因为“道具”对象没有改变。注意整个 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"));
于 2021-08-17T08:36:24.560 回答