useMemo和useCallback
1.useMemo缓存计算结果,缓存函数
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
参数:接受一个函数(返回需要缓存的值)和一个依赖数组(依赖项变化时重新计算)。
返回值:返回函数计算的值,依赖未变化时直接复用缓存。
核心场景:
昂贵计算:对大量数据排序、过滤、复杂数学运算等,避免每次渲染都重新计算。
const sortedList = useMemo(() => { return bigData.sort((a, b) => a.value - b.value); // 大数据排序}, [bigData]);
引用稳定性:当需要将一个对象/数组作为 props 传递给子组件(且子组件用
React.memo
优化时),避免因每次渲染生成新引用导致子组件无效更新。const config = useMemo(() => ({ timeout: 1000, retry: 3 }), []); // 配置对象保持引用不变
2.useCallback缓存函数
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
参数:接受一个函数(需要缓存的函数)和一个依赖数组(依赖项变化时重新创建函数)。
返回值:返回缓存的函数实例,依赖未变化时复用之前的函数。
核心场景:
子组件优化:当函数作为 props 传递给子组件(尤其是用
React.memo
包裹的子组件)时,避免因函数引用变化导致子组件不必要的重新渲染。const handleClick = useCallback(() => {console.log('Button clicked', count); }, [count]); // 依赖 count,count 变化时才更新函数return <MemoizedChild onClick={handleClick} />; // MemoizedChild 用 React.memo 包裹
避免闭包陷阱:在定时器、事件监听等异步场景中,确保函数引用稳定,避免捕获到过期的状态。
useEffect(() => {const timer = setInterval(() => {console.log('Current value:', stableValue); // 使用稳定的函数引用}, 1000);return () => clearInterval(timer); }, [stableValue]);
区别
3. 依赖管理复杂
2. 合理使用 React.memo 配合
如果你需要 缓存一个计算结果(比如过滤后的列表、格式化后的数据),用
useMemo
。如果你需要 缓存一个函数(比如传递给子组件的回调),用
useCallback
。本质上,useCallback 是 useMemo 的一种特例(当缓存的函数返回值是函数本身时,两者可互换,但语义更清晰)。
三、滥用 useMemo/useCallback 的问题
1. 性能反而下降
额外开销:
useMemo
和useCallback
本身需要维护缓存逻辑,首次渲染时会有额外的计算/创建成本,且依赖项变化时需要对比依赖数组。如果缓存的值或函数本身计算成本很低(比如简单的加法、返回常量),使用它们反而会增加不必要的复杂度。// ❌ 滥用:计算简单,无优化必要 const doubled = useMemo(() => num * 2, [num]); // num * 2 是简单计算,直接写 num * 2 更清晰 const handleClick = useCallback(() => { setCount(count + 1); // 简单函数,无子组件依赖,直接定义即可 }, [count]);
2. 代码可读性降低
过度优化:滥用会导致代码中充斥大量
useMemo
/useCallback
,增加维护成本和理解成本。掩盖真正问题:过度依赖缓存可能掩盖组件设计不合理(如子组件未合理拆分、状态管理混乱)的本质问题。
依赖数组错误:如果依赖项遗漏或错误(比如漏掉某个依赖),会导致缓存失效(
useMemo
重新计算,useCallback
重新创建函数),引发 bug。而错误的依赖管理比直接重新计算/创建函数的代价更高。// ❌ 错误示例:漏掉依赖 count,导致函数内使用过期的 count const handleClick = useCallback(() => { console.log(count); // 若 count 是依赖但未声明,这里可能拿到旧值 } , []); // 漏了 [count]
四、最佳实践建议
1. 优先考虑代码可读性与维护性
默认不使用:大多数情况下,直接写普通函数或计算逻辑更清晰,除非有明确的性能瓶颈或引用稳定性需求。
按需使用:仅在以下场景使用:
子组件用
React.memo
优化,且需要稳定函数引用(用useCallback
)。计算逻辑复杂且依赖项变化频繁(用
useMemo
)。需要避免闭包陷阱(如定时器、事件监听中的函数)。
子组件优化:如果子组件是纯展示组件(props 变化才重新渲染),用
React.memo
包裹,再配合useCallback
缓存回调函数,才能真正减少渲染。const Child = React.memo(({ onClick }) => {console.log('Child 渲染'); // 仅当 onClick 引用变化或 props 变化时渲染return <button onClick={onClick}>点击</button>; });function Parent() {const handleClick = useCallback(() => {console.log('点击事件');}, []); // 依赖为空,函数引用永远不变return <Child onClick={handleClick} />; }