我们应该在 React 功能组件中的每个函数处理程序中使用 useCallback

Should we use useCallback in every function handler in React Functional Components

假设我们有这样的组件

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

正如我从 post 的回答中读到的那样:

简短的回答是因为每次都重新创建箭头函数,这会影响性能。 post 提出的一种解决方案是用空数组包装在 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)还是 always 将其包装在 useCallback 中???

The short answer is because arrow function is recreated every time, which will hurt the performance.

这是一个常见的误解。箭头函数每次 无论哪种方式 都会重新创建(尽管 useCallback 随后的可能会立即被丢弃)。 useCallback 的作用是让您使用回调的子组件在被记忆后不会 re-render。

先看看误区吧。考虑 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 都必须 re-render;它不能在渲染之间重复使用。但是如果 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 and 的方式频繁更改时,这可能是有意义的 如果 Button 在渲染时必须做大量工作。 Button 可能不会,但其他子组件可能会。

例如,如果您使用 count 作为按钮的文本,我之前示例中的 useCallback 可能毫无意义,因为这意味着 Button 必须 re-render 不管怎样:

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)。但是ComponentB中的那个总是要使用setter、() => setCount(count => count + 1)的回调形式。这是因为如果您继续使用您创建的第一个 increment,它关闭的 count 将会过时 - 您会看到计数变为 1,但不会再进一步​​。


最后一点:如果您 re-render 经常使用一个组件,以至于创建和丢弃它的各种功能可能会导致过多的内存流失(罕见 情况),你可以通过使用 ref 来避免它。让我们看看将 ComponentB 更新为使用 useCallback:

的 ref intead
const incrementRef = useRef(null);
if (!incrementRef.current /* || yourDependenciesForItChange*/) {
    // 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
    incrementRef.current = () => setCount(count => count + 1);
}
const increment = incrementRef.current;

它只会创建一次 increment 函数(在那个例子中,因为我们没有任何依赖项),它不会像使用 useCallback 那样创建和丢弃函数。之所以起作用,是因为ref的初始值是null,然后第一次调用组件函数,我们看到是null,创建函数,放到ref上。所以 increment 只创建一次。

每次调用 increment 时,该示例都会重新创建我们传递给 setCount 的函数。也可以避免这种情况:

const incrementRef = useRef(null);
if (!incrementRef.current) {
    // 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 incrementCallback = count => count + 1;
    incrementRef.current = () => setCount(incrementCallback);
}
const increment = incrementRef.current;

const { useState, useRef } = 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);
    const incrementRef = useRef(null);
    if (!incrementRef.current) {
        // 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 incrementCallback = count => count + 1;
        incrementRef.current = () => setCount(incrementCallback);
    }
    const increment = incrementRef.current;
    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>

就避免不必要的函数创建而言,这真的达到了 11。 :-)

这是一个罕见的组件,甚至需要第一级优化,更不用说第二级了;但是 when/if 你就是这样做的。

在我看来,useCallback不是为了表现。我想不出定义一个函数真的很昂贵的任何原因。 与 useMemo 不同,useCallback 只是记忆函数,并不实际执行它。

那么什么时候使用呢?

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

TL 博士;仅当函数需要位于 useEffect

的依赖数组中时才使用 useCallback

目前我能想到的有两种情况:

  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]);

(如果函数不是async,我们可以直接使用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. 何时应将函数属性传递给 3rd 方组件:
const onAwesomeLibarayLoaded = useCallback(() => {
  doSomething(state1, state2);
}, [state1, state2]);

<AwesomeLibrary 
  onLoad={onAwesomeLibarayLoaded}
/>

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

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

如果你确定它不在useEffect里面那么即使你不使用useCallback也是可以的。