22

假设我们有这样的组件

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

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

error    JSX props should not use arrow functions        react/jsx-no-bind

正如我从这篇文章的答案中读到的: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>
  );
}

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

这让我真的很困惑?所以对于功能组件,在处理内联函数处理程序时,我应该只编写箭头函数(忽略 eslint)还是总是将它包装在 useCallback 中?

4

2 回答 2

39

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

这是一个普遍的误解。无论哪种方式,每次都会重新创建箭头函数(尽管useCallback可能会立即丢弃后续函数)。这样做useCallback的目的是使您使用回调的子组件在被记忆时不会重新渲染。

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

const increment = useCallback(() => setCounter(counter => counter + 1), []);

执行如下:

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

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

  3. 用这两个参数调用useCallback,取回一个函数

与不使用时所拥有的进行比较useCallback

const increment = () => setCounter(counter => counter + 1);

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

让我们继续讨论useCallback实际有用的东西。让我们看看回调在哪里使用:

<Button onClick={increment} />

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

这是一个例子:

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")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

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

你想什么时候这样做?这在很大程度上取决于您,但是当您的组件状态以不影响内容的方式频繁更改时,这可能是有意义的increment,因此不影响渲染Button 是否Button必须做大量工作。Button可能不会,但其他子组件可能会。

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

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>
            <Button onClick={increment}>{count}</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>
            <Button onClick={increment}>{count}</Button>
        </div>
    );
}

ReactDOM.render(
    <div>
        A:
        <ComponentA />
        B:
        <ComponentB />
    </div>,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

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

于 2020-09-30T09:31:46.703 回答
-1

在我看来,useCallback不是为了性能。我想不出定义一个函数真的很昂贵的任何原因。与 不同useMemouseCallback只是记住函数而不实际执行它。

那么我们应该什么时候使用它呢?

主要用例是防止不必要地重新运行功能。重新定义一个函数没有问题,但是在每次状态更新时重新运行它是错误的,而且通常很危险。

TL博士;仅useCallback在函数需要位于依赖数组中时使用useEffect

我现在能想到的有两种情况:

  1. 例如,一个函数是异步的,我们需要在任何依赖项发生更改时运行它:
const [data, setData] = useState([]);
const [filter, setFilter] = useState({});

const fetchData = useCallback(async () => {
  const response = await fetchApi(filter);
  setData(response.data);
}, [filter]);

useEffect(() => {
  fetchData();
}, [fetchData]);

(如果函数不是异步的,我们可以useEffect不使用直接使用useCallback

useCallback但是,当它仅由用户交互运行时无需包装它:

const [data, setData] = useState([]);
const [filter, setFilter] = useState({});

const fetchData = async () => {
  const response = await fetchApi(filter);
  setData(response.data);
};

return (
  <button onClick={fetchData}>Fetch Data</button>
);
  1. 当您应该将函数 prop 传递给 3rd-party 组件时:
const onAwesomeLibarayLoaded = useCallback(() => {
  doSomething(state1, state2);
}, [state1, state2]);

<AwesomeLibrary 
  onLoad={onAwesomeLibarayLoaded}
/>

因为组件可能会使用传递函数AwesomeLibrary执行类似于示例 1 的操作:onLoad

const AwesomeLibarary = ({onLoad}) => {
  useEffect(() => {
    // do something
    onLoad();
  }, [onLoad]);
};

如果你确定它不在里面,useEffect那么即使你不使用它也没关系useCallback

于 2021-07-05T16:30:48.560 回答