WHAT - useCallback 深入理解
文章目录
- 一句话理解
- 场景 1:防止子组件不必要的重新渲染
- 场景 2:在 `useEffect` 中依赖一个函数
- 场景 3:结合自定义 Hook 传入回调
- 场景 4:配合 `useMemo` / 复杂计算逻辑
- 场景 5:与性能优化库结合(例如虚拟列表、表格)
- 注意事项
- 总结
在 WHAT - React 函数与 useMemo vs useCallback 我们简单了解过 useCallback。今天我们来深入理解一下。
useCallback 是 React 中一个非常重要但容易被误解的 Hook。今天我们通过 多个典型场景 来理解它的用途和原理。
一句话理解
useCallback(fn, deps)会返回一个 记忆化(缓存)版本的回调函数,
只有当依赖项 (deps) 变化时,才会返回新的函数引用。
场景 1:防止子组件不必要的重新渲染
这是最常见的场景。
问题
当父组件重新渲染时,所有在它内部定义的函数都会被重新创建。
import React, { useState } from 'react';const Child = React.memo(({ onClick }: { onClick: () => void }) => {console.log('✅ Child render');return <button onClick={onClick}>Click me</button>;
});export default function App() {const [count, setCount] = useState(0);const handleClick = () => console.log('clicked');return (<div><p>Count: {count}</p><button onClick={() => setCount(count + 1)}>+1</button><Child onClick={handleClick} /></div>);
}
👉 每次点击“+1”,父组件重新渲染,handleClick 被重新创建(函数引用变了)。
即使 Child 用了 React.memo,它也会重新渲染,因为 onClick 变了。
解决:使用 useCallback
const handleClick = useCallback(() => console.log('clicked'), []);
✅ 现在 handleClick 的引用在依赖 [] 不变时保持稳定。
父组件更新时,Child 不会重新渲染。
场景 2:在 useEffect 中依赖一个函数
如果你把函数直接写在组件里,而又在 useEffect 中依赖它,会导致无限循环。
useEffect(() => {fetchData();
}, [fetchData]); // 🚨 fetchData 每次都是新函数,会导致无限请求
✅ 使用 useCallback 固定函数引用:
const fetchData = useCallback(() => {// fetch something
}, []);useEffect(() => {fetchData();
}, [fetchData]); // ✅ 不会无限循环
场景 3:结合自定义 Hook 传入回调
例如在一个自定义 Hook 里需要外部传一个回调。
function useEventListener(event: string, handler: () => void) {useEffect(() => {window.addEventListener(event, handler);return () => window.removeEventListener(event, handler);}, [event, handler]);
}
⚠️ 如果 handler 每次都是新函数(没用 useCallback),
那每次渲染都会移除再添加一次事件监听。
✅ 改用:
const handleKeyDown = useCallback(() => {console.log('Pressed key');
}, []);useEventListener('keydown', handleKeyDown);
场景 4:配合 useMemo / 复杂计算逻辑
比如你在一个 memoized 的计算中依赖一个回调:
const computeValue = useCallback((x: number) => x * 2, []);
const result = useMemo(() => computeValue(10), [computeValue]);
这样可以确保 computeValue 不会频繁变化导致 useMemo 重新计算。
场景 5:与性能优化库结合(例如虚拟列表、表格)
像 Ant Design Table、react-window 等组件会用到 onRow, onClick, rowRenderer 等回调。
如果不使用 useCallback,每次渲染都会创建新的函数,造成性能浪费或无法缓存。
const onRowClick = useCallback((record) => {console.log('Clicked row:', record);
}, []);
注意事项
-
不是所有函数都要用
useCallback-
它会增加内存开销和心智负担。
-
一般只在以下场景使用:
- 函数传给
React.memo的子组件; - 函数作为
useEffect依赖; - 函数传给性能敏感组件(虚拟列表、大表格等)。
- 函数传给
-
-
依赖数组很重要!
- 若函数中引用了外部状态变量,必须把它们加进依赖数组中。
const handleClick = useCallback(() => {console.log(count); }, [count]); // ✅ count 是依赖 -
useCallback(fn, deps)≈useMemo(() => fn, deps)- 本质上
useCallback是useMemo的一个语法糖。
- 本质上
总结
| 场景 | 是否适合用 useCallback | 说明 |
|---|---|---|
| 子组件 props 是函数且使用 React.memo | ✅ | 避免子组件重复渲染 |
| 函数放在 useEffect 依赖中 | ✅ | 避免无限循环 |
| 自定义 Hook 接受回调 | ✅ | 避免重复副作用 |
| 函数只在本组件使用 | ❌ | 没必要缓存 |
| 简单 onClick/onChange | ❌ | 直接写即可 |
如果你愿意,我可以帮你写一个 交互式 demo(计数器 + 子组件 + useCallback对比),
让你在浏览器里实时看到差异。
是否要我帮你写一个这样的例子?
