简而言之,因为每次都会重新创建箭头函数,这会损害性能。
这是一个普遍的误解。每次都会重新创建箭头函数无论哪种方式(虽然与useCallback
后续的可能会立即扔掉)。什么useCallback
这样做的目的是使您使用回调的子组件在被记忆后不会重新渲染。
我们先来看看错误的观念。考虑useCallback
call:
const increment = useCallback(() => setCounter(counter => counter + 1), []);
执行起来是这样的:
-
评估第一个参数,() => setCounter(counter => counter + 1)
, 创建一个函数
-
评估第二个参数,[]
,创建一个数组
-
Call 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
and if 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
不是免费的,它会影响回调中的代码。查看回调中的代码ComponentA
and ComponentB
在示例中。ComponentA
(这不使用useCallback
)可以使用的值count
它会关闭(在限制范围内!),() => setCount(count + 1)
。但其中的一个ComponentB
总是必须使用setter的回调形式,() => setCount(count => count + 1)
。那是因为如果你继续使用第一个increment
你创造的,count
它关闭就会过时——您会看到计数变为 1,但不会再进一步了。
最后一点:如果您经常重新渲染组件,以至于创建并丢弃您传递给的各种函数useCallback
or useMemo
可能会导致过多的内存搅动(arare情况),您可以通过使用 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
仅创建一次。
该示例确实重新创建了我们传递的函数setCount
每次increment
叫做。也可以避免这种情况:
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。 :-)
这是一个罕见的组件,甚至需要第一级优化,更不用说第二级优化了;但当/如果你这样做,那就是你这样做的方式。