在 React 应用开发中,随着组件复杂度的提升,性能问题逐渐显现。尤其当组件中包含大量计算逻辑或频繁触发重渲染时,应用响应速度会明显下降。React 提供了 useMemo 和 useCallback 两个核心 Hook,配合 React.memo,可以有效避免不必要的重复计算和组件重渲染,显著提升应用性能。本文将通过三个典型 Demo,深入剖析这两个 Hook 的使用场景、原理及最佳实践。
一、昂贵计算的缓存:useMemo 的作用
1.1 问题场景:无意义的重复计算
考虑以下组件:
javascript
import { useState } from "react";
export default function App() {
const [x, setX] = useState(1);
const [y, setY] = useState(1);
console.time('xxx');
// 模拟大量的计算
let a = 0;
for (let j = 0; j < 100000000; j++) {
a += j;
}
console.timeEnd('xxx');
return (
<>
<h1>x: {x}</h1>
<h1>y: {y}</h1>
<h1>a: {a}</h1>
<button onClick={() => setX(x + 1)}>x + 1</button>
<button onClick={() => setY(y + 1)}>y + 1</button>
</>
);
}
在这个组件中,每次点击按钮(无论是 x + 1 还是 y + 1),都会触发组件重新渲染。而每次渲染时,都会执行一次耗时的循环计算(1亿次加法)。然而,这个计算结果 a 实际上与 x 和 y 无关------它始终是固定的值(即 0 到 99999999 的累加和)。因此,这种重复计算完全是无意义的,严重浪费 CPU 资源。
1.2 解决方案:使用 useMemo 缓存结果
useMemo 的核心思想是记忆化(memoization) :将计算结果缓存起来,只有当依赖项发生变化时才重新计算。
javascript
import { useState, useMemo } from "react";
export default function App() {
const [x, setX] = useState(1);
const [y, setY] = useState(1);
console.time('xxx');
const memoA = useMemo(() => {
let a = 0;
for (let j = 0; j < 100000000; j++) {
a += j;
}
return a;
}, []); // 依赖项为空数组
console.timeEnd('xxx');
return (
<>
<h1>x: {x}</h1>
<h1>y: {y}</h1>
<h1>a: {memoA}</h1>
<button onClick={() => setX(x + 1)}>x + 1</button>
<button onClick={() => setY(y + 1)}>y + 1</button>
</>
);
}
关键点在于 useMemo 的第二个参数------依赖数组 。这里传入空数组 [],表示该计算不依赖任何外部变量 ,因此只在组件首次挂载时执行一次。后续无论 x 或 y 如何变化,memoA 都直接返回缓存的值,不再执行耗时循环。控制台输出的时间将从几百毫秒降至几乎为 0。
注意 :
useMemo返回的是计算结果本身 ,而非副作用函数(这与useEffect不同)。必须用变量接收其返回值才能使用。
1.3 动态依赖:让缓存"聪明"起来
如果计算逻辑依赖于状态变量,就必须将该变量加入依赖数组:
ini
const memoA = useMemo(() => {
let a = 0;
for (let j = 0; j < 100000000; j++) {
a += y; // 计算依赖 y
}
return a;
}, [y]); // 依赖项为 [y]
此时:
- 当点击 x + 1 时,
y未变 →useMemo返回缓存值 → 无计算开销。 - 当点击 y + 1 时,
y发生变化 →useMemo重新执行计算 → 产生时间消耗。
若错误地将依赖数组设为空 [],则即使 y 改变,useMemo 仍会返回首次计算的旧值(因为闭包捕获了初始的 y),导致数据不一致 。这就是典型的"闭包陷阱"------依赖数组必须包含回调函数中用到的所有外部变量。
二、避免子组件无谓重渲染:useCallback + React.memo
2.1 问题根源:函数引用变化
在组件通信中,父组件常需向子组件传递回调函数。但每次父组件重渲染时,都会创建一个全新的函数引用:
javascript
// 父组件每次渲染都会生成新函数
const handleClick = () => {
console.log('click');
};
即使函数逻辑完全相同,JavaScript 也会将其视为不同对象。当子组件使用 React.memo 优化时,浅比较会认为 props 发生了变化,从而触发不必要的重渲染。
2.2 React.memo:子组件的"守门员"
React.memo 是一个高阶组件,用于包裹函数组件。它通过浅比较 props 来决定是否跳过渲染:
javascript
const Child = memo(({ count, handleClick }) => {
console.log('child 重新渲染');
return <div onClick={handleClick}>{count} 子组件</div>;
});
- 若
count和handleClick的引用均未改变 → 跳过渲染。 - 若任一 prop 引用改变 → 重新渲染。
2.3 useCallback:稳定函数引用
为解决函数引用变化问题,useCallback 应运而生。它专门用于缓存函数引用:
ini
const handleClick = useCallback(() => {
console.log('click');
}, [count]); // 依赖项为 [count]
工作原理:
-
首次渲染:创建函数并缓存。
-
后续渲染:
- 若
count未变 → 返回缓存的函数引用。 - 若
count改变 → 创建新函数并更新缓存。
- 若
结合 React.memo,可实现精准渲染控制:
javascript
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
const handleClick = useCallback(() => {
console.log('click');
}, [count]);
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>count + 1</button>
{num}
<button onClick={() => setNum(num + 1)}>num + 1</button>
<Child count={count} handleClick={handleClick} />
</div>
);
}
测试行为:
- 点击 num + 1 :
count不变 →handleClick引用不变 →Child不重渲染。 - 点击 count + 1 :
count改变 →handleClick生成新引用 →Child重渲染。
关键点 :
useCallback(fn, deps)等价于useMemo(() => fn, deps)。前者是后者的语法糖,专用于函数缓存。
三、核心概念对比与最佳实践
3.1 useMemo vs useCallback
| 特性 | useMemo | useCallback |
|---|---|---|
| 用途 | 缓存计算结果(值) | 缓存函数引用 |
| 返回值 | 回调函数的返回值 | 回调函数本身 |
| 典型场景 | 昂贵计算(如大数据处理、复杂过滤) | 传递给子组件的回调函数 |
| 等价写法 | --- | useMemo(() => fn, deps) |
3.2 依赖数组的黄金法则
无论是 useMemo 还是 useCallback,依赖数组都必须遵循:
- 完整性:包含回调中用到的所有外部变量(状态、props、其他 Hooks 的返回值等)。
- 必要性:仅包含真正影响计算/函数行为的变量,避免过度依赖导致缓存失效。
违反完整性会导致闭包陷阱(使用过期值);过度依赖则削弱缓存效果。
3.3 性能优化的边界
- 不要过早优化 :仅对已知的性能瓶颈使用
useMemo/useCallback。简单计算或小型组件无需优化。 - 避免滥用空依赖 :
[]仅适用于完全静态的值。若计算涉及 props 或状态,必须正确声明依赖。 - React.memo 的局限性 :仅对 props 进行浅比较。若 prop 是对象/数组,需确保其引用稳定(同样可用
useMemo缓存)。
四、总结
useMemo 和 useCallback 是 React 性能优化体系中的重要工具:
useMemo通过缓存昂贵计算的结果,避免重复执行相同逻辑。useCallback通过稳定函数引用,配合React.memo阻止子组件无谓重渲染。
二者的核心机制均依赖于依赖数组的精确声明------这是避免 bug 与发挥性能优势的关键。在实际开发中,应结合具体场景判断是否需要优化,并始终遵循依赖声明的完整性原则。合理运用这些 Hook,能让 React 应用在保持代码简洁的同时,获得流畅的用户体验。