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

os8fio9y  于 2022-12-03  发布在  React
关注(0)|答案(1)|浏览(118)

假设我们有这样的组件

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

正如我从这篇文章的回答中读到的:这个函数是一个垃圾收集器,它是一个垃圾收集器。
简单的回答是因为每次都要重新创建箭头函数,这会影响性能。这篇文章提出的一个解决方案是用一个useCallback钩子 Package ,并使用空数组。当我改为这样时,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)还是总是将其 Package 在useCallback中???

2vuwiymt

2vuwiymt1#

简短的回答是因为每次都重新创建箭头函数,这会损害性能。
这是一个常见的误解。arrow函数每次都被重新创建无论如何(尽管使用useCallback,后续的函数可能会立即被丢弃)。useCallback所做的是使你使用回调的子组件在被记忆化时不会重新呈现。
让我们先来看看误解,考虑一下useCallback调用:

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

它是这样执行的:
1.计算第一个参数() => setCounter(counter => counter + 1)创建函数
1.计算第二个参数[],创建一个数组
1.用这两个参数调用useCallback,返回一个函数
与不使用useCallback时的结果进行比较:

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

那就简单多了:创建函数,这样就不必执行上面的#2和#3。
让我们来看看useCallback实际上做了什么有用的事情。

<Button onClick={increment} />

现在,假设Button被存储为React.memo或类似的存储,如果increment在每次组件呈现时都改变,那么Button必须在每次组件改变时重新呈现;但是如果increment在渲染之间是稳定的(因为你使用了useCallback和一个空数组),那么调用Button的记忆结果可以被重用,不需要再次调用。
下面是一个例子:
第一个
请注意,单击ComponentA中的按钮始终会再次调用Button,但单击ComponentB中的按钮不会。
这在很大程度上取决于你,但是当你的组件的状态频繁改变,并且不影响increment的内容,也不影响Button时,如果Button在呈现时必须做大量的工作,那么这可能是有意义的。Button可能不会,但是其他子组件可能会。
例如,如果您使用count作为按钮的文本,则我前面的示例中的useCallback可能没有意义,因为这意味着无论如何Button都必须重新呈现:
一个
还要注意,useCallback不是免费的,它会影响回调中的代码。请查看示例中ComponentAComponentB回调中的代码。ComponentA(它不使用useCallback)可以使用它关闭的count的值但是ComponentB中的函数必须使用setter的回调形式() => setCount(count => count + 1),这是因为如果你一直使用你创建的第一个increment,它关闭的count将失效-您将看到计数变为1,但不会再进一步。
最后一点:如果您频繁地重新呈现组件,以至于创建和丢弃传递给useCallbackuseMemo的各种函数可能会导致过多的内存混乱(***罕见***情况),您可以通过使用ref来避免这种情况。让我们看看如何将ComponentB更新为使用ref而不是useCallback

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。:-)
这是一个罕见的组件,需要甚至第一级的优化,更不用说第二级;但是如果你做了,那就是你做。

相关问题