在 React 开发中,随着应用复杂度的提升,性能问题逐渐显现。频繁的组件重渲染、重复的昂贵计算、不必要的函数重建……这些问题虽小,却可能成为拖慢用户体验的“隐形杀手”。
幸运的是,React 提供了两个强大的 Hook —— useMemo 和 useCallback,它们是函数组件中进行细粒度性能优化的核心工具。本文将带你深入理解它们的原理、适用场景与常见误区,并通过真实代码示例掌握最佳实践。
一、为什么需要性能优化?
React 的核心理念是 “状态驱动视图” 。当状态(state)或属性(props)发生变化时,组件会重新执行其函数体,生成新的 UI。
但这带来一个问题:
即使某些数据未变,整个组件也会重新运行,导致不必要的计算和子组件重渲染。
例如:
- 一个搜索框输入关键词,只应过滤列表,而不应影响其他无关状态(如计数器)
- 一个子组件只依赖某个特定 prop,但父组件其他状态变化却导致它被重新渲染
这就是 useMemo 和 useCallback 大显身手的地方。
二、useMemo:缓存计算结果
✅ 作用
避免在每次渲染时重复执行昂贵的计算逻辑,仅在依赖项变化时才重新计算。
📌 语法
js
编辑
const memoizedValue = useMemo(() => {
// 执行计算并返回结果
}, [dep1, dep2]);
🎯 典型场景
- 过滤/映射大型数组
- 复杂数学运算
- 派生状态(类似 Vue 的
computed)
🔧 示例:避免无效的列表过滤
jsx
预览
const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange'];
// ❌ 每次组件渲染都会执行 filter(包括 count 变化时!)
// const filterList = list.filter(item => item.includes(keyword));
// ✅ 仅当 keyword 变化时才重新过滤
const filterList = useMemo(() => {
return list.filter(item => item.includes(keyword));
}, [keyword]); // 依赖 keyword
💡 注意:
""(空字符串)调用includes("")会返回true,这是 JavaScript 的正常行为,需根据业务判断是否需要处理。
⚠️ 警告:不要滥用
- 对于简单计算(如
a + b),useMemo的开销可能大于收益 - 只用于真正“昂贵”的操作
三、useCallback:缓存函数引用
✅ 作用
防止函数在每次渲染时被重新创建,保持函数引用稳定,从而避免破坏子组件的 memo 优化。
📌 语法
js
编辑
const memoizedFn = useCallback(() => {
// 函数逻辑
}, [dep1, dep2]);
🎯 典型场景
- 将函数作为 prop 传递给
memo包裹的子组件 - 作为
useEffect、useMemo的依赖项
🔧 示例:配合 memo 避免子组件无效重渲染
jsx
预览
// 子组件使用 memo 优化
const Child = memo(({ count, handleClick }) => {
console.log('Child 渲染'); // 我们希望它只在 count 或 handleClick 逻辑变化时才打印
return <div onClick={handleClick}>子组件 {count}</div>;
});
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// ❌ 每次渲染都创建新函数 → Child 总是重渲染
// const handleClick = () => { console.log('click'); };
// ✅ 缓存函数,仅当 count 变化时更新(若函数依赖 count)
const handleClick = useCallback(() => {
console.log('click', count); // 如果用到 count,必须加入依赖
}, [count]);
return (
<div>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<button onClick={() => setNum(num + 1)}>num + 1</button> {/* 不影响 Child */}
<Child count={count} handleClick={handleClick} />
</div>
);
}
✅ 效果:点击 “num + 1” 时,
Child不会重新渲染,因为它的 props(count和handleClick)均未变化。
四、useMemo vs useCallback:本质关系
其实,useCallback 是 useMemo 的语法糖:
js
编辑
// 这两行等价
const fn = useCallback(callback, deps);
const fn = useMemo(() => callback, deps);
useMemo缓存 任意值useCallback专门缓存 函数
五、常见误区与最佳实践
❌ 误区 1:所有函数都包 useCallback
不需要! 只有当函数作为 prop 传给
memo组件,或作为其他 Hook 的依赖时,才有必要。
❌ 误区 2:遗漏依赖项
js
编辑
const handleClick = useCallback(() => {
console.log(count); // 用到了 count
}, []); // ❌ 忘记依赖 count → 闭包捕获旧值!
✅ 正确做法:把所有用到的响应式变量加入依赖数组。
❌ 误区 3:过度优化
过早优化是万恶之源。先写出清晰的代码,再用 React DevTools 分析性能瓶颈,最后针对性优化。
✅ 最佳实践口诀:
“用到就加依赖,不用不加;昂贵才缓存,简单别乱搞。”
六、总结
表格
| Hook | 用途 | 核心价值 |
|---|---|---|
useMemo | 缓存计算结果 | 避免重复执行昂贵计算 |
useCallback | 缓存函数引用 | 配合 memo 防止子组件无效重渲染 |
它们共同服务于 React 的性能优化哲学:
“只在必要时更新,其余时间保持静默。”
掌握 useMemo 和 useCallback,不仅能写出更高效的 React 应用,更能深入理解 React 的响应式机制与闭包陷阱。但请记住:工具再好,也要用在刀刃上。
🌟 延伸思考:
如果你的组件中有多个派生状态或复杂逻辑,是否考虑将部分逻辑抽离为自定义 Hook?这能让代码更可维护,也更容易复用优化策略。
希望这篇文章能帮你真正掌握这两个 Hook!