引言
在现代 React 应用开发中,随着组件逻辑日益复杂、状态管理愈发庞大,性能问题 逐渐成为开发者绕不开的话题。幸运的是,React 提供了两个强大而精巧的 Hooks ------ useMemo 和 useCallback,它们如同"缓存魔法",帮助我们在不牺牲可读性的前提下,显著提升应用性能。
本文将结合完整代码示例,逐行解析、对比说明、深入原理 ,带你彻底掌握 useMemo 与 useCallback 的使用场景、工作机制、常见误区以及最佳实践。文章内容力求全面、准确、生动有趣 ,并严格保留原始代码一字不变,确保你既能理解理论,又能直接复用实战。
一、为什么需要 useMemo 和 useCallback?
1.1 React 函数组件的"重运行"特性
在 React 中,每当组件的状态(state)或 props 发生变化时,整个函数组件会重新执行一遍。这意味着:
- 所有变量都会重新声明;
- 所有函数都会重新定义;
- 所有计算逻辑都会重新跑一次。
这本身是 React 响应式更新机制的核心,但也会带来不必要的开销。
💡 关键洞察 :
"组件函数重新运行" ≠ "DOM 重新渲染"。
React 会通过 Virtual DOM diff 算法决定是否真正更新 DOM。
但昂贵的计算 或子组件的无谓重渲染,仍可能拖慢应用。
二、useMemo:为"昂贵计算"穿上缓存外衣
2.1 什么是"昂贵计算"?
看这段代码:
bash
// 昂贵的计算
function slowSum(n) {
console.log('计算中...')
let sum = 0
for(let i = 0; i < n*10000; i++){
sum += i
}
return sum
}
这个 slowSum 函数执行了 n * 10000 次循环!如果 n=100,就是一百万次加法。在每次组件重渲染时都调用它,用户界面可能会卡顿。
2.2 不用 useMemo 的后果
假设我们这样写:
ini
const result = slowSum(num); // ❌ 每次渲染都重新计算!
那么,即使你只是点击了 count + 1 按钮(与 num 无关),slowSum 依然会被执行!因为整个 App 函数重新运行了。
2.3 useMemo 如何拯救性能?
React 提供 useMemo 来记忆(memoize)计算结果:
scss
const result = useMemo(() => {
return slowSlow(num)
}, [num])
✅ 工作原理:
第一次渲染:执行函数,缓存结果。
后续渲染:检查依赖项
[num]是否变化。
- 如果
num没变 → 直接返回缓存值,不执行函数体。- 如果
num变了 → 重新执行函数,更新缓存。
2.4 完整上下文中的 useMemo 使用
javascript
export default function App(){
const [num, setNum] = useState(0)
const [count, setCount] = useState(0)
const [keyword, setKeyword] = useState('')
const list = ['apple','banana', 'orange', 'pear']
// ✅ 仅当 keyword 改变时才重新过滤
const filterList = useMemo(() => {
return list.filter(item => item.includes(keyword))
}, [keyword])
// ✅ 仅当 num 改变时才重新计算 slowSum
const result = useMemo(() => {
return slowSum(num)
}, [num])
return (
<div>
<p>结果: {result}</p>
<button onClick={() => setNum(num + 1)}>num + 1</button>
<input type="text" value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
{
filterList.map(item => (
<li key={item}>{item}</li>
))
}
</div>
)
}
🔍 重点观察:
点击 "count + 1" 时:
slowSum不会执行(因为num没变);filterList不会重新计算(因为keyword没变);- 控制台不会打印 "计算中..." 或隐含的 "filter执行"。
这就是
useMemo带来的精准缓存!
2.5 关于 includes 和 filter 的小贴士
"apple".includes("")确实返回true(空字符串是任何字符串的子串);list.filter(...)返回的是一个新数组 ,即使结果为空(如[]),它也是一个新的引用。
⚠️ 正因如此,如果不使用
useMemo,每次渲染都会生成一个新数组引用 ,可能导致依赖该数组的子组件误判为 props 变化而重渲染!
三、useCallback:为"回调函数"打造稳定身份
3.1 问题起源:函数是"新"的!
在 JavaScript 中,每次函数定义都会创建一个新对象:
javascript
// 每次 App 重运行,handleClick 都是一个全新函数!
const handleClick = () => { console.log('click') }
即使函数体完全一样,handleClick !== previousHandleClick。
3.2 子组件为何"无辜重渲染"?
看这段代码:
javascript
const Child = memo(({count, handleClick}) => {
console.log('child重新渲染')
return (
<div onClick={handleClick}>
<h1>子组件 count: {count}</h1>
</div>
)
})
memo的作用:浅比较 props,若没变则跳过渲染。- 但每次父组件重渲染,
handleClick都是新函数 → props 引用变了 →memo失效 → 子组件重渲染!
即使你只改了 num,Child 也会重渲染,尽管它只关心 count!
3.3 useCallback 的解决方案
useCallback 本质上是 useMemo 的语法糖,专用于缓存函数:
javascript
const handleClick = useCallback(() => {
console.log('click')
}, [count])
✅ 效果:
- 只要
count不变,handleClick的引用就保持不变;Child的 props 引用未变 →memo生效 → 跳过重渲染!
3.4 完整 useCallback 示例
javascript
import {
useState,
memo,
useCallback
} from 'react'
const Child = memo(({count, handleClick}) => {
console.log('child重新渲染')
return (
<div onClick={handleClick}>
<h1>子组件 count: {count}</h1>
</div>
)
})
export default function App(){
const [count, setCount] = useState(0)
const [num, setNum] = useState(0)
// ✅ 缓存函数,依赖 count
const handleClick = useCallback(() => {
console.log('click')
}, [count])
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<p>num: {num}</p>
<button onClick={() => setNum(num + 1)}>num + 1</button>
<Child count={count} handleClick={handleClick} />
</div>
)
}
🔍 行为验证:
- 点击 "num + 1":
Child不会打印 "child重新渲染";- 点击 "count + 1":
Child会 重渲染(因为count和handleClick都变了);- 如果
handleClick不依赖count(依赖项为[]),则只有count变化时Child才重渲染。
四、useMemo vs useCallback:一张表说清区别
| 特性 | useMemo |
useCallback |
|---|---|---|
| 用途 | 缓存任意值(数字、数组、对象等) | 缓存函数 |
| 本质 | useMemo(fn, deps) |
useMemo(() => fn, deps) 的简写 |
| 典型场景 | 昂贵计算、过滤/映射大数组、创建复杂对象 | 传递给 memo 子组件的事件处理器 |
| 返回值 | 函数执行的结果 | 函数本身 |
| 错误用法 | 用于无副作用的纯计算 | 用于依赖外部变量但未声明依赖 |
💡 记住 :
useCallback(fn, deps)≡useMemo(() => fn, deps)
五、常见误区与最佳实践
❌ 误区1:到处使用 useMemo/useCallback
-
不要为了"可能的优化"而滥用。
-
缓存本身也有开销(存储、比较依赖项)。
-
只在以下情况使用:
- 计算确实昂贵(如大数据处理);
- 导致子组件无谓重渲染(配合
memo); - 作为 props 传递给已优化的子组件。
❌ 误区2:依赖项遗漏
scss
const handleClick = useCallback(() => {
console.log(count) // 依赖 count
}, []) // ❌ 错误!应该写 [count]
这会导致函数捕获旧的 count 值(闭包陷阱)。
✅ 正确做法:所有外部变量都必须出现在依赖数组中。
✅ 最佳实践
- 先写逻辑,再优化:不要过早优化。
- 配合 React DevTools Profiler:定位真实性能瓶颈。
- useMemo 用于值,useCallback 用于函数。
- 依赖项要完整且精确 :使用 ESLint 插件
eslint-plugin-react-hooks自动检查。
六、总结:性能优化的哲学
useMemo 和 useCallback 并非银弹,而是 React 赋予我们的精细控制权。它们让我们能够:
- 隔离变化:让无关状态的更新不影响其他部分;
- 减少冗余:避免重复计算和渲染;
- 提升用户体验:使应用更流畅、响应更快。
正:
"count 和 keyword 不相关"
"某一个数据改变,只想让相关的子组件重新渲染"
这正是 React 性能优化的核心思想:局部更新,全局协调。
附:完整代码地址
源码地址:react/memo/memo/src/App.jsx · Zou/lesson_zp - 码云 - 开源中国
🎉 掌握
useMemo与useCallback,你已经迈入 React 性能优化的高手之列!下次遇到"为什么子组件总在乱渲染?"或"计算太慢怎么办?",你就知道答案了。
Happy coding! 🚀