useCallback/useMemo
下面,我们来系统的梳理关于 React useCallback & useMemo 的基本知识点:
一、性能优化基础概念
1.1 React 渲染机制回顾
- 渲染流程:组件状态/props变化 → 重新执行组件函数 → 生成新虚拟DOM → 与旧DOM对比 → 更新实际DOM
- 性能瓶颈:不必要的重新渲染(re-render)和重复计算
1.2 引用相等性(Referential Equality)
const a = { value: 1 };
const b = { value: 1 };
console.log(a === b); // false - 不同内存引用const c = a;
console.log(a === c); // true - 相同引用
- JavaScript中对象、数组、函数每次创建都是新引用
- React中依赖引用相等性判断是否需要更新
二、useMemo 深度解析
2.1 基本概念
useMemo
缓存计算结果,避免每次渲染重复计算
2.2 核心语法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- 参数1:计算函数,返回需要缓存的值
- 参数2:依赖数组,只有依赖变化时才重新计算
2.3 工作原理
graph LR
A[组件渲染] --> B{依赖项是否变化?}
B -->|是| C[重新执行计算函数]
B -->|否| D[返回缓存值]
C --> E[缓存新值并返回]
2.4 使用场景
场景1:昂贵计算缓存
function Chart({ data }) {// 避免每次渲染重新计算数据转换const transformedData = useMemo(() => {return transformData(data); // 耗时操作}, [data]);return <svg>{/* 使用 transformedData 渲染 */}</svg>;
}
场景2:避免子组件不必要重渲染
function Parent({ a, b }) {// 对象缓存,保证引用不变const config = useMemo(() => ({ color: 'red', size: a }), [a]);return <Child config={config} value={b} />;
}// 配合React.memo
const Child = React.memo(({ config, value }) => {/* 只在config或value变化时重渲染 */
});
2.5 注意事项
- 不要滥用:计算不昂贵时,直接计算可能比useMemo开销更小
- 依赖必须完整:遗漏依赖可能导致使用过期值
- 副作用禁止:计算函数应为纯函数,无副作用
三、useCallback 深度解析
3.1 基本概念
useCallback
缓存函数引用,避免每次渲染创建新函数
3.2 核心语法
const memoizedCallback = useCallback(() => {doSomething(a, b);
}, [a, b]);
- 参数1:需要缓存的函数
- 参数2:依赖数组,依赖变化时返回新函数
3.3 与useMemo关系
// 两者等效
useCallback(fn, deps) === useMemo(() => fn, deps)
3.4 使用场景
场景1:避免子组件不必要重渲染
const Child = React.memo(({ onClick }) => {/* 依赖onClick */
});function Parent() {const [count, setCount] = useState(0);// 缓存函数引用const handleClick = useCallback(() => {console.log('点击');}, []); // 空依赖:始终相同return <Child onClick={handleClick} />;
}
场景2:作为其他Hook的依赖
function Search({ query }) {const [results, setResults] = useState([]);const fetchResults = useCallback(async () => {const data = await fetch(`/search?q=${query}`);setResults(await data.json());}, [query]); // query变化时重新创建useEffect(() => {fetchResults();}, [fetchResults]); // 依赖函数// ...
}
3.5 注意事项
- 依赖处理:函数内部使用的state/props必须声明为依赖
- 闭包陷阱:缓存函数捕获创建时的上下文
- 必要性判断:子组件未用React.memo优化时无效
四、性能优化策略
4.1 何时使用 useMemo
- 计算成本高的操作(如大数据转换、复杂计算)
- 引用类型值作为props传递给优化组件
- 作为其他Hook的依赖项
4.2 何时使用 useCallback
- 函数作为props传递给优化组件
- 函数作为其他Hook的依赖项
- 事件处理函数被多次绑定(如列表项)
4.3 优化组件配合
// 使用React.memo进行组件记忆
const OptimizedComponent = React.memo(function Component(props) {/* 只在props变化时重渲染 */
});// 默认浅比较,可自定义比较函数
React.memo(Component, (prevProps, nextProps) => {/* 返回true不重渲染,false重渲染 */
});
五、高级模式
5.1 依赖项管理技巧
函数依赖稳定化
const [count, setCount] = useState(0);// 方法1:使用useCallback
const increment = useCallback(() => setCount(c => c + 1), []);// 方法2:使用dispatch(useReducer)
const increment = () => dispatch({ type: 'increment' });
对象依赖最小化
// 不推荐:依赖整个对象
useEffect(() => {}, [user]);// 推荐:只依赖必要属性
useEffect(() => {}, [user.id]);
5.2 自定义Hook封装
function useStableCallback(fn) {const ref = useRef();// 始终使用最新函数ref.current = fn;return useCallback((...args) => {return ref.current?.(...args);}, []);
}// 使用:避免依赖变化
const handleClick = useStableCallback(() => {console.log(props.data);
});
六、常见问题与解决方案
6.1 依赖循环问题
场景:useCallback依赖state,state更新触发函数重建,导致依赖该函数的useEffect循环执行
解决:使用函数式更新或useReducer
// 错误示例
const [data, setData] = useState([]);
const fetchData = useCallback(async () => {const res = await fetch('/api');setData(await res.json());
}, []); // 缺少setData依赖,但ESLint会警告// 正确方案1:使用函数式更新避免依赖
const fetchData = useCallback(async () => {const res = await fetch('/api');setData(prev => [...prev, ...await res.json()]);
}, []);// 正确方案2:使用useRef保存函数
const dataRef = useRef();
dataRef.current = data;
const fetchData = useCallback(async () => {// 使用dataRef.current
}, []);
6.2 闭包过期问题
场景:useCallback捕获创建时的旧状态
解决:
const [count, setCount] = useState(0);// 方法1:使用函数式更新
const increment = useCallback(() => {setCount(prev => prev + 1);
}, []);// 方法2:使用ref保存最新值
const countRef = useRef(count);
countRef.current = count;const logCount = useCallback(() => {console.log(countRef.current);
}, []);
6.3 何时不用优化
- 简单组件:渲染成本低于优化开销
- 频繁变化的值:缓存意义不大
- 顶层组件:无父组件触发重渲染
七、案例
7.1 表单优化
function Form({ onSubmit }) {const [values, setValues] = useState({});// 缓存事件处理const handleChange = useCallback((e) => {setValues(v => ({ ...v, [e.target.name]: e.target.value }));}, []);// 缓存提交处理const handleSubmit = useCallback((e) => {e.preventDefault();onSubmit(values);}, [values, onSubmit]);return (<form onSubmit={handleSubmit}><input name="name" onChange={handleChange} /><input name="email" onChange={handleChange} /><button type="submit">提交</button></form>);
}
7.2 虚拟列表优化
function VirtualList({ items, itemHeight }) {const [scrollTop, setScrollTop] = useState(0);// 计算可见项(避免重复计算)const visibleItems = useMemo(() => {const startIndex = Math.floor(scrollTop / itemHeight);const endIndex = startIndex + 10;return items.slice(startIndex, endIndex);}, [items, scrollTop, itemHeight]);// 缓存滚动处理const handleScroll = useCallback((e) => {setScrollTop(e.currentTarget.scrollTop);}, []);return (<div className="list" onScroll={handleScroll}><div style={{ height: `${items.length * itemHeight}px` }}>{visibleItems.map(item => (<ListItem key={item.id} item={item} />))}</div></div>);
}
7.3 复杂状态派生
function Dashboard({ transactions }) {// 缓存复杂计算const summary = useMemo(() => {return transactions.reduce((acc, transaction) => {acc.total += transaction.amount;acc.categories[transaction.category] = (acc.categories[transaction.category] || 0) + transaction.amount;return acc;}, { total: 0, categories: {} });}, [transactions]);return (<div><TotalAmount value={summary.total} /><CategoryChart data={summary.categories} /></div>);
}
八、总结
8.1 决策流程图
graph TD
A[需要优化吗?] -->|是| B{优化什么?}
B -->|计算| C[useMemo]
B -->|函数| D[useCallback]
A -->|否| E[直接使用]
8.2 黄金法则
- 先测量后优化:使用React DevTools分析性能
- 避免过早优化:只在必要时使用
- 确保正确性:完整声明依赖项
- 组合优化:配合React.memo使用
8.3 性能优化全景图
优化技术 | 适用场景 | 工具 |
---|---|---|
React.memo | 避免子组件不必要重渲染 | 组件包装 |
useMemo | 缓存昂贵计算/稳定引用 | Hook |
useCallback | 缓存函数引用 | Hook |
虚拟化 | 长列表渲染 | react-window |
懒加载 | 代码/组件延迟加载 | React.lazy |