我们应该在 React Functional Components 的每个函数处理程序中使用 useCallback

Ngo*_*Lam 14 javascript typescript reactjs arrow-functions usecallback

假设我们有这样的组件

const Example = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = () => setCounter(counter => counter + 1); 
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}
Run Code Online (Sandbox Code Playgroud)

当我将onClick处理程序作为箭头函数传递时,我eslint抛出一个警告:

error    JSX props should not use arrow functions        react/jsx-no-bind
Run Code Online (Sandbox Code Playgroud)

正如我从这篇文章的答案中读到的:https : //stackoverflow.com/questions/36677733/why-shouldnt-jsx-props-use-arrow-functions-or-bind# :~: text=Why%20you%20shouldn 't%20use,previous%20function%20is%20garbage%20collected

简短的回答是因为每次都会重新创建箭头函数,这会损害性能。这篇文章提出的一个解决方案是用空数组包装在useCallback钩子中。当我改成这个时,eslint 警告就真的消失了。

const Example = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = useCallback(() => setCounter(counter => counter + 1), []);
  
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}
Run Code Online (Sandbox Code Playgroud)

然而,也有另一种观点认为过度使用useCallback 最终会因为 useCallback 的开销而降低性能。一个例子在这里:https : //kentcdodds.com/blog/usememo-and-usecallback

这真的让我很困惑吗?因此,对于函数式组件,在处理内联函数处理程序时,我应该只编写箭头函数(忽略 eslint)还是始终将其包装在 useCallback 中?

T.J*_*der 26

简短的回答是因为每次都会重新创建箭头函数,这会损害性能。

这是一个普遍的误解。箭头功能重建每次无论哪种方式(虽然useCallback后来者可以立即扔掉)。什么useCallback是使您使用回调的子组件有可能在它被记忆时不重新渲染。

我们先来看看这个误解。考虑useCallback调用:

const increment = useCallback(() => setCounter(counter => counter + 1), []);
Run Code Online (Sandbox Code Playgroud)

是这样执行的:

  1. 计算第一个参数() => setCounter(counter => counter + 1)创建一个函数

  2. 计算第二个参数[],创建一个数组

  3. 打电话useCallback与这两个参数,得到的回复功能

如果您不使用,请与您拥有的进行比较useCallback

const increment = () => setCounter(counter => counter + 1);
Run Code Online (Sandbox Code Playgroud)

这要简单得多:创建函数。然后它不必执行上面的#2 和#3。

让我们继续讨论什么useCallback是有用的。我们来看看回调的用处:

<Button onClick={increment} />
Run Code Online (Sandbox Code Playgroud)

现在,假设ButtonReact.memo或相似。如果increment你的组件Button每次渲染都发生变化,那么每次你的组件发生变化时都必须重新渲染;它不能在渲染之间重复使用。但是如果increment在渲染之间是稳定的(因为你使用useCallback了一个空数组),调用的记忆结果Button可以被重用,它不必再次调用。

下面是一个例子:

const increment = useCallback(() => setCounter(counter => counter + 1), []);
Run Code Online (Sandbox Code Playgroud)
const increment = () => setCounter(counter => counter + 1);
Run Code Online (Sandbox Code Playgroud)

请注意,单击中的按钮ComponentA始终会Button再次调用,但单击中的按钮ComponentB不会。

你想什么时候这样做?这主要是取决于你,但它可能是有道理的,当你组件的状态将在不影响内容的方式变化频繁increment,因此不会影响Button 是否Button有做显著工作渲染时。Button可能不会,但其他子组件可能会。

例如,useCallback如果您将count用作按钮的文本,则在我之前的示例中可能毫无意义,因为这意味着Button无论如何都必须重新渲染:

<Button onClick={increment} />
Run Code Online (Sandbox Code Playgroud)
const { useState, useCallback } = React;

const Button = React.memo(function Button({onClick, children}) {
    console.log("Button called");
    return <button onClick={onClick}>{children}</button>;
});

function ComponentA() {
    console.log("ComponentA called");
    const [count, setCount] = useState(0);
    // Note: Safe to use the closed-over `count` here if `count `updates are
    // triggered by clicks or similar events that definitely render, since
    // the `count` that `increment` closes over won't be stale.
    const increment = () => setCount(count + 1);
    return (
        <div>
            {count}
            <Button onClick={increment}>+</Button>
        </div>
    );
}

function ComponentB() {
    console.log("ComponentB called");
    const [count, setCount] = useState(0);
    // Note: Can't use `count` in `increment`, need the callback form because
    // the `count` the first `increment` closes over *will* be slate after
    // the next render
    const increment = useCallback(
        () => setCount(count => count + 1),
        []
    );
    return (
        <div>
            {count}
            <Button onClick={increment}>+</Button>
        </div>
    );
}

ReactDOM.render(
    <div>
        A:
        <ComponentA />
        B:
        <ComponentB />
    </div>,
    document.getElementById("root")
);
Run Code Online (Sandbox Code Playgroud)

另请注意,这useCallback不是免费的,它会影响回调中的代码。查看示例中ComponentAComponentB示例中的回调中的代码。ComponentA(不使用useCallback)可以使用的价值count,它关闭了(限制内!) () => setCount(count + 1)。但是 inComponentB总是必须使用 setter 的回调形式,() => setCount(count => count + 1). 那是因为如果你继续使用increment你创建的第一个,count它关闭的将是陈旧的——你会看到计数变为 1,但永远不会更远。