新开发者经常听到或读到 useCallback
和 useMemo
是提高 React 应用程序 性能
的工具。
因此,他们开始在任何地方使用它们,希望优化他们的应用程序。但这通常会导致代码混乱、过于冗长,充满复杂的依赖数组------讽刺的是,这往往弊大于利。
在这篇文章中,我们将探讨 useCallback
和 useMemo
实际上做什么,何时它们有用,以及------更重要的是------何时避免使用它们。
让我们了解一下 useCallback
和 useMemo
的作用:
useCallback: "记住"你传递给它的函数,这样 React 就不会在每次组件更新时创建该函数的全新版本。
tsx
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []);
在上面的代码片段中,只要依赖数组([])不改变,handleClick 在渲染过程中将保持不变。如果没有
useCallback
,每次渲染都会创建一个新函数。
useMemo: 让 React 记住(或"缓存")计算的结果,这样它就不会在每次组件更新时重新计算。
tsx
const expensiveValue = useMemo(() => {
return computeHeavyValue(input);
}, [input]);
在这里,
computeHeavyValue(input)
只在 input 改变时运行。如果在下次渲染时 input 相同,React 会重用缓存的结果而不是重新计算。
做这些事情(当 props 改变时记住你的值)是有代价的。
useCallback
和 useMemo
的隐藏成本
使用 useCallback 和 useMemo 背后的核心思想是记忆化并不是免费的------这是一种权衡。你花费额外的 CPU 周期和内存来缓存函数或值,希望在未来的渲染中节省更昂贵的计算。但如果使用不当,记忆化的成本可能会超过收益。
1. 内存开销: 这是最直接的成本。当你使用 useCallback
或 useMemo
时,React 必须在渲染之间将记忆化的函数或值存储在内存中。
2. CPU 开销: 在 React 决定是使用缓存值还是重新运行函数之前,React 必须遍历依赖数组并对每个项目与上一次渲染的值进行浅比较。
3. 可读性和维护性: 记忆化每个函数或值会创建冗长的代码,难以阅读和理解。这些 hooks 强制开发者仔细管理依赖数组。忘记或添加新的依赖可能会创建微妙的 bug。
tsx
const memoizedFn = useCallback(() => {
doSomething(a, b);
}, [a]); // 依赖中缺少 b --- 可能导致 bug
何时不要优化
让我们看一个使用 useCallback
不必要的情况:
tsx
const logEvent = useCallback(() => {
analytics.track('button_click');
}, []);
<button onClick={logEvent}>Click me</button>
在这个例子中,logEvent 函数很简单,并且没有传递给任何
记忆化的子组件
。因此,使用 useCallback 没有提供真正的好处,只是增加了不必要的复杂性。
tsx
const UserInput = React.memo(({ onChange }) => {
const handleChange = useCallback(event => {
onChange(event.target.value);
}, [onChange]);
return <input onChange={handleChange} />;
});
在上面的例子中,
handleChange
总是基于onChange
创建,这是唯一的 prop,所以当父组件重新渲染并传递新的onChange
prop 时,React.memo
和useCallback
都会触发重新渲染并重新创建handleChange
。如果onChange
没有改变,React.memo
会阻止重新渲染,所以handleChange
也不会重新计算------即使没有useCallback
。所以这里useCallback
没有被正确使用。
tsx
const _debounce = (func, delay) => { /* returns debounced function */ };
const makeApiCall = (e) => { console.log("Making an API call"); };
const debounce = useCallback(_debounce(makeApiCall, 2000), []);
<input onChange={e => debounce(e.target.value)} />
尽管使用了
useCallback
,函数每次都会被重新定义(因为函数调用是内联的),所以所有的记忆化都是无意义的,并增加了 CPU 成本和内存使用。
现在让我们探讨使用 useMemo
不必要的情况:
- 记忆化小计算
tsx
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.completed);
}, [todos]);
为什么这是过度的:这里的
.filter
非常快,内联使用它也会更具可读性。useMemo
在这里增加了内存开销和依赖复杂性。
- 记忆化 JSX 树
tsx
const todoRows = useMemo(() => {
return todos.map(todo => <ToDoRow key={todo.id} todo={todo} />);
}, [todos]);
useMemo
只是存储一个值------但 React 仍然会重新渲染每个子组件,除非它们用React.memo
记忆化。
何时优化
useCallback、useMemo 可以是强大的工具------但只有在正确的情况下使用时。
- 使用 useMemo 优化昂贵的计算
tsx
const filteredUsers = useMemo(() => {
// 昂贵的过滤逻辑
return users.filter(user => user.name.toLowerCase().includes(searchTerm.toLowerCase()));
}, [users, searchTerm]);
在上面的例子中,
useMemo
记忆化了过滤后的用户列表,所以过滤函数只有在 users 数组或 searchTerm 改变时才会再次运行。这种优化可以通过避免每次渲染时的不必要重新计算来显著提高性能,即使在大数据集的情况下也能保持 UI 响应性。
- 使用 useCallback 为子组件提供稳定的回调
tsx
const handleUserSelect = useCallback((userId) => {
// 选择用户的逻辑
setSelectedUserId(userId);
}, [setSelectedUserId]);
在这个例子中,
useCallback
记忆化了handleUserSelect
函数,使其引用只有在setSelectedUserId
改变时才会改变。这种稳定性防止依赖 handleUserSelect 的子组件重新渲染,除非真正必要,提高了渲染效率和整体应用性能。
- useCallback 和 useMemo 一起用于关联操作
tsx
const filteredUsers = useMemo(() => {
return users.filter(user => user.name.toLowerCase().includes(searchTerm.toLowerCase()));
}, [users, searchTerm]);
const handleUserSelect = useCallback((userId) => {
setSelectedUserId(userId);
}, [setSelectedUserId]);
return (
<UserList users={filteredUsers} onUserSelect={handleUserSelect} />
);
useMemo
记忆化过滤后的用户列表,避免在每次渲染时重新计算过滤数组,除非 users 或 searchTerm 改变。useCallback
记忆化handleUserSelect
函数以保持其引用稳定,防止接收它作为 prop 的子组件不必要的重新渲染。
通过结合这两个 hooks,你确保子组件 <UserList>
接收到稳定的数据和稳定的回调,这有助于 React 跳过不必要的渲染,提高性能并保持 UI 响应性。
总结
useCallback
和 useMemo
是强大的 hooks,可以提升 React 应用性能,但只有在深思熟虑地应用时才有效。过度使用它们会导致复杂、难以维护的代码,有时甚至会带来更差的性能。
总是问自己:
-
函数或值的计算是否昂贵?
-
它是否传递给依赖稳定引用的记忆化子组件?
-
记忆化是否会有意义地减少不必要的渲染或重新计算?
如果答案是肯定的,useCallback
和 useMemo
可以是很好的盟友。如果不是,保持代码简单清晰通常更好地为你服务。
记住,过早优化
是万恶之源------只有在你知道问题存在时才进行优化!