我正在研究 React 项目,并且正在研究一些库。我发现他们使用的“useCallback”与我使用的不同。 下面是该代码部分。我仍然认为这段代码与直接使用“useCallback”没有什么区别
// Saves incoming handler to the ref in order to avoid "useCallback hell"
export function useEventCallback<T, K>(handler?: (value: T, event: K) => void): (value: T, event: K) => void {
const callbackRef = useRef(handler);
useEffect(() => {
callbackRef.current = handler;
});
return useCallback((value: T, event: K) => callbackRef.current && callbackRef.current(value, event), []);
}
所以我的问题是“useCallback hell”是什么意思?这样使用“useCallback”有什么好处?
// 顺便说一句:我在 React 文档中找到了类似的示例。但我还是不明白 https://en.reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
当您执行正常的
useCallback
操作时,您必须传入一个包含函数使用的变量的依赖项数组。当其中之一发生变化时,记忆就会中断。这在很多情况下都很好,但有时你的记忆总是会被破坏(因为你依赖于不断变化的值)。当这种情况发生时,useCallback
几乎没有任何好处。
您展示的代码的目标是记忆化永远不会中断,即使您有复杂的依赖关系。请注意,当它调用 useCallback
时,它会传入一个空的依赖项数组
[]
。与使用 ref 相结合,可以跟踪最新的
handler
是什么。然后,当最终调用该函数时,它将检查最新的
handler
的 ref 并调用它。最新的
handler
在其闭包中具有最新值,因此它的行为符合预期。这段代码确实实现了永不破坏记忆的目标。然而,它需要小心使用。如果你使用的是react的并发渲染,并且在渲染过程中调用
useEventCallback
返回的函数,你可能会得到一些意想不到的结果。只有在渲染之外调用该函数才是安全的,例如在事件回调中,这就是他们将其命名为
useEventCallback
的原因。
组件体内的函数
function App() {
const [count, setCount] = useState(0);
const lastRenderTime = new Date().toString();
function bodyFn() {
alert("bodyFn: " + lastRenderTime);
}
return (
<>
Last render time: {lastRenderTime}
<SomeChildComponent onClick={bodyFn} />
</>
);
}
每当 <App>
组件重新渲染时(例如,如果
count
状态被修改),就会创建一个新的
bodyFn
。应该
<SomeChildComponent>
监视其
onClick
道具引用(通常在依赖项数组中),它每次都会看到新的创建。但事件回调行为符合预期:每当调用
bodyFn
时,它都是该函数的“最近创建”,特别是它正确使用
lastRenderTime
的最新值(与已显示的相同)。
useCallback
的常见用法
function App() {
const lastRenderTime = new Date().toString();
const ucbFn = useCallback(
() => alert("ucbFn: " + lastRenderTime),
[lastRenderTime]
);
return (
<>
Last render time: {lastRenderTime}
<SomeChildComponent onClick={ucbFn} />
</>
);
}
每当 <App>
重新渲染时,
lastRenderTime
值都会不同。因此,
useCallback
依赖数组启动,为
ucbFn
创建一个新的更新函数。与之前的情况一样,
<SomeChildComponent>
每次都会看到这种变化。而且使用
useCallback
似乎毫无意义!但至少,行为也符合预期:该函数始终是最新的,并显示正确的
lastRenderTime
值。尝试避免
useCallback
依赖
function App() {
const lastRenderTime = new Date().toString();
const ucbFnNoDeps = useCallback(
() => alert("ucbFnNoDeps: " + lastRenderTime),
[] // Attempt to avoid cb modification by emptying the dependency array
);
return (
<>
Last render time: {lastRenderTime}
<SomeChildComponent onClick={ucbFnNoDeps} />
</>
);
}
在恢复 useCallback
优势的“天真的”尝试中,人们可能会试图从其依赖项数组中删除
lastRenderTime
。现在
ucbFnNoDeps
确实总是一样的,并且
<SomeChildComponent>
不会看到任何变化。但是现在,行为不再像人们预期的那样:
ucbFnNoDeps
读取
lastRenderTime
创建时在其范围内的值,即<App>
渲染时的第一个时间!带有定制
useEventCallback
挂钩
function App() {
const lastRenderTime = new Date().toString();
const uecbFn = useEventCallback(
() => alert("uecbFn: " + lastRenderTime),
);
return (
<>
Last render time: {lastRenderTime}
<SomeChildComponent onClick={uecbFn} />
</>
);
}
每当 <App>
重新渲染时,就会创建一个新的箭头函数(
useEventCallback
自定义钩子的参数)。但钩子内部只是将其存储在其
useRef
当前占位符中。
uecbFn
中的钩子返回函数永远不会改变。所以
<SomeChildComponent>
没有看到任何变化。但是恢复了最初预期的行为:当执行回调时,它会查找当前占位符内容,即最近创建的箭头函数。因此使用最新的
lastRenderTime
值!
<SomeChildComponent>
的示例
function SomeChildComponent({
onClick,
}: {
onClick: () => void;
}) {
const countRef = useRef(0);
useEffect(
() => { countRef.current += 1; },
[onClick] // Increment count when onClick reference changes
);
return (
<div>
onClick changed {countRef.current} time(s)
<button onClick={onClick}>click</button>
</div>
)
}
CodeSandbox 上的演示: