React性能优化三剑客:useEffect、useMemo与useCallback实战手册
废话不多说,直接上干货,兄弟们,注意了,我要开始装逼了:
一、useEffect:副作用管理
核心作用
处理与渲染无关的副作用(如数据获取、订阅事件、定时器、DOM 操作等),并支持清理逻辑。
基本语法
useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑(可选)
};
}, [依赖项]);
使用场景
-
数据获取
useEffect(() => { fetch("/api/data") .then(res => setData(res)); }, []); // 仅在挂载时获取 -
事件监听
useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); // 清理监听器 -
定时器
useEffect(() => { const timer = setInterval(() => setCount(c => c + 1), 1000); return () => clearInterval(timer); }, []); // 清理定时器
最佳实践
- 依赖数组:精确控制执行时机,避免遗漏依赖导致闭包问题。
- 清理函数:移除订阅、清除定时器,防止内存泄漏。
-
避免无限循环:若副作用中修改状态,使用函数式更新(如
setCount(prev => prev + 1))。
二、useMemo:计算结果缓存
核心作用
缓存复杂计算结果,避免重复执行高开销操作(如排序、过滤、深拷贝)。
基本语法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
使用场景
-
复杂计算
const filteredList = useMemo(() => list.filter(item => item.price > 100), [list] // 仅当 list 变化时重新计算 ); -
避免重复渲染
const userInfo = useMemo(() => ({ name, age }), [name, age]); return <Child user={userInfo} />; // 稳定引用,避免子组件重渲染
最佳实践
- 依赖项完整:包含所有影响计算结果的变量。
- 避免滥用:简单计算无需缓存,直接执行即可。
- **配合
React.memo**:缓存子组件,提升渲染性能。
三、useCallback:函数引用缓存
核心作用
缓存函数引用,避免因父组件重新渲染导致子组件不必要的重渲染。
基本语法
const memoizedFn = useCallback(() => {
doSomething(a, b);
}, [a, b]);
使用场景
-
传递回调函数
const handleClick = useCallback(() => { console.log("Clicked"); }, []); return <Button onClick={handleClick} />; -
**配合
useEffect**const fetchData = useCallback(async () => { const data = await fetch("/api"); setData(data); }, []); useEffect(() => fetchData(), [fetchData]);
最佳实践
- 依赖项精准:确保函数内部使用的变量均包含在依赖数组中。
-
避免过度优化:仅在需要稳定引用时使用(如传递给
React.memo子组件)。
四、三者的核心区别
| Hook | 核心用途 | 返回值 | 典型场景 |
|---|---|---|---|
useEffect |
处理副作用(数据获取、订阅) | 无(返回清理函数) | API 请求、事件监听 |
useMemo |
缓存计算结果 | 计算后的值 | 复杂计算、避免重复渲染 |
useCallback |
缓存函数引用 | 函数 | 传递回调、优化子组件渲染 |
五、useEffect 最佳实践与闭包陷阱规避
1. 依赖项管理
-
核心原则:依赖数组必须包含所有在 effect 中使用的 外部变量(包括 state、props、上下文等)。
// 错误示例:未包含依赖项,导致闭包捕获旧值 const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); // 始终输出初始值 0 }, 1000); return () => clearInterval(timer); }, []); // ❌ 依赖数组为空 // 正确做法:将 count 加入依赖数组 useEffect(() => { const timer = setInterval(() => { console.log(count); // 实时输出最新值 }, 1000); return () => clearInterval(timer); }, [count]); // ✅问题根源:未声明依赖项时,effect 仅在挂载时执行一次,闭包中捕获的
count是初始值。
2. 清理函数
-
场景:定时器、事件监听、DOM 操作等需在组件卸载或依赖变化时清理。
useEffect(() => { const handleResize = () => console.log("窗口大小变化"); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); // ✅ 清理监听器 }, []);
3. 避免无限循环
-
问题:effect 中修改状态且依赖该状态。
// 错误示例:依赖项包含 state,导致循环触发 const [data, setData] = useState([]); useEffect(() => { fetchData().then((res) => setData(res)); // 触发重新渲染 → effect 重新执行 }, [data]); // ❌解决方案:移除冗余依赖,或使用函数式更新:
useEffect(() => { fetchData().then((res) => setData((prev) => [...prev, ...res])); // ✅ 无依赖 }, []);
4. 复杂异步操作
-
最佳实践:在 effect 内部定义异步函数,避免直接
async修饰回调。useEffect(() => { let isMounted = true; // 防止卸载后更新状态 const fetchData = async () => { const res = await api.getData(); if (isMounted) setData(res); }; fetchData(); return () => { isMounted = false; }; // 清理标志位 }, []);
六、useMemo 和 useCallback 的真正使用场景
1. useMemo:缓存计算结果
-
适用场景:
- 高开销计算:如大数据排序、复杂公式、深拷贝。
- 派生状态:根据 state/props 生成新对象或数组。
-
DOM 节点引用:如
useRef存储 DOM 元素(需配合useLayoutEffect)。
-
避免滥用:
- 简单计算(如
a + b)无需缓存。 - 频繁变化的依赖项(如
useMemo依赖项变化频繁时反而降低性能)。
// 正确使用:缓存复杂计算 const filteredData = useMemo(() => { return data.filter(item => item.price > 100); }, [data]); // 错误使用:缓存简单值 const simpleValue = useMemo(() => 42, []); // ❌ 无意义 - 简单计算(如
2. useCallback:缓存函数引用
-
适用场景:
-
传递回调给子组件:子组件使用
React.memo时,避免因父组件重渲染导致子组件无效更新。 - 依赖外部变量的函数:如事件处理器中依赖 state/props。
-
传递回调给子组件:子组件使用
-
避免滥用:
- 仅在需要稳定引用时使用(如传递给
useEffect或子组件)。 - 内部函数无需缓存(如仅在组件内部使用的函数)。
// 正确使用:缓存事件处理器 const handleClick = useCallback(() => { console.log("Clicked"); }, []); // 错误使用:缓存内部函数 const internalFn = useCallback(() => { // 仅在组件内部使用,无需缓存 }, []); - 仅在需要稳定引用时使用(如传递给
七、避免过度优化的关键原则
1. 性能问题驱动优化
- 先定位瓶颈:使用 React DevTools 的 Profiler 分析组件渲染开销。
-
针对性优化:仅在渲染耗时或计算密集型场景使用
useMemo/useCallback。
2. 依赖项管理
-
完整声明:使用 ESLint 的
react-hooks/exhaustive-deps规则强制检查。 -
避免冗余依赖:如
useMemo依赖项中包含未使用的变量。
3. 简单场景优先
-
优先使用普通变量:如
const value = someData;而非useMemo(() => someData, [])。 - 函数内部使用:无需缓存仅在组件内部调用的函数。
八、实战案例对比
场景:商品列表筛选
-
未优化版本(频繁重渲染):
const ProductList = ({ products, filter }) => { const filtered = products.filter(/* 复杂逻辑 */); return <List items={filtered} />; }; -
优化后版本:
const ProductList = ({ products, filter }) => { const filtered = useMemo(() => { return products.filter(/* 复杂逻辑 */); }, [products, filter]); // 仅当数据或筛选条件变化时重新计算 return <List items={filtered} />; };
九、总结
| Hook | 核心用途 | 避坑指南 |
|---|---|---|
useEffect |
副作用管理(数据获取、订阅) | 依赖项完整、清理函数、避免循环 |
useMemo |
缓存计算结果 | 仅高开销计算、避免简单值缓存 |
useCallback |
缓存函数引用 | 仅传递给 memo 子组件、避免内部函数 |
关键口诀:
“无必要,不优化;有性能问题,再针对性解决”。过度使用 Hooks 反而会增加代码复杂度和内存开销。