5月28日 02:51

useCallback 和 useMemo 有什么区别?什么场景下使用?

面试官问:"useCallback 和 useMemo 有什么区别?什么场景下使用?"——这道题几乎出现在每一场 React 岗位的面试中。答案其实不复杂,但很多人答完区别就卡在使用场景上,要么说"都用上总没错",要么完全不知道什么时候该用。

核心区别:一句话记住

useCallback 缓存函数引用,useMemo 缓存计算结果。

jsx
// 这两行等价 const fn = useCallback(() => doSomething(a), [a]); const fn = useMemo(() => () => doSomething(a), [a]);

useCallback(fn, deps) 本质上就是 useMemo(() => fn, deps) 的语法糖。记住这个等式,很多困惑会自动消散。

特征useCallbackuseMemo
返回值函数本身函数的执行结果
缓存对象函数引用任意值(对象、数组、基本类型)
典型场景传给子组件的回调昂贵计算 / 保持引用稳定
一句话"别重新创建这个函数""别重新算这个值"

为什么需要它们:React 重渲染机制

React 函数组件每次渲染都会重新执行整个函数体。你在组件里写的每一行代码——定义变量、创建函数、计算表达式——每次渲染都重新跑一遍:

jsx
function MyComponent({ items }) { const handleClick = () => console.log("clicked"); // 每次渲染都创建新函数 const filtered = items.filter(item => item.active); // 每次渲染都重新过滤 return <Child onClick={handleClick} data={filtered} />; }

如果 items 有 10000 条,而组件每秒渲染 60 次,你就在每秒过滤 60 万次。更麻烦的是,handleClick 每次都是新的引用——如果 Child 用了 React.memo,它期待的"不变引用"就白费了。

useCallback 和 useMemo 的作用就是告诉 React:"如果依赖没变,把上次的结果还给我。"

useCallback:保持函数引用稳定

配合 React.memo 阻止子组件无效渲染

这是 useCallback 最核心的使用场景:

jsx
function Parent({ items }) { // ❌ 每次渲染都创建新函数,Child 的 React.memo 白费了 const handleClick = () => { console.log("clicked"); }; // ✅ 函数引用稳定,Child 不会因它而重渲染 const handleClick = useCallback(() => { console.log("clicked"); }, []); return <Child onClick={handleClick} items={items} />; } const Child = React.memo(({ onClick, items }) => { console.log("Child render"); return <button onClick={onClick}>Click</button>; });

关键点:useCallback 单独用效果有限,必须配合 React.memo 才能阻止子组件重渲染。如果子组件没有 React.memo 包裹,父组件渲染它就会渲染——useCallback 改变不了这一点。

作为 useEffect 的稳定依赖

jsx
function UserProfile({ userId }) { // ❌ fetchUser 每次都是新引用,useEffect 每次都会执行 const fetchUser = () => { api.getUser(userId); }; // ✅ 只在 userId 变化时重新创建 const fetchUser = useCallback(() => { api.getUser(userId); }, [userId]); useEffect(() => { fetchUser(); }, [fetchUser]); }

不过这里有个陷阱:用 useCallback 缓存函数再放进 useEffect 依赖,实际上你是间接依赖了 callback 的真实依赖。对于上面的例子,直接在 useEffect 里写请求逻辑、直接依赖 userId 更直接。这个模式主要用在自定义 Hook 中,函数需要暴露给外部使用:

jsx
function useUserData(userId) { const fetchUser = useCallback(() => { return api.getUser(userId); }, [userId]); return { fetchUser }; // 调用方拿到的引用是稳定的 }

自定义 Hook 中的 useCallback

这是面试中容易忽略的场景。当你写一个自定义 Hook 返回方法时,如果不加 useCallback,消费方每次拿到的都是新函数,它的 useEffect 会被反复触发:

jsx
function useTable(pagination) { const refresh = useCallback(() => { fetchData(pagination); }, [pagination]); const reset = useCallback(() => { setFilters({}); setPagination({ page: 1 }); }, []); return { refresh, reset }; // 消费方可以安全地放入依赖数组 }

useMemo:缓存计算结果

避免重复的数组操作

jsx
function ProductList({ products, filter }) { // ❌ 每次渲染都重新过滤 const filteredProducts = products.filter(p => p.name.includes(filter)); // ✅ 只在 products 或 filter 变化时重新计算 const filteredProducts = useMemo(() => products.filter(p => p.name.includes(filter)), [products, filter] ); return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>; }

复杂计算(排序、聚合、数据转换)

jsx
function DataTable({ data }) { const sortedData = useMemo(() => { return [...data].sort((a, b) => a.score - b.score); }, [data]); const chartData = useMemo(() => { return data.reduce((acc, item) => { const key = item.category; if (!acc[key]) acc[key] = { total: 0, count: 0 }; acc[key].total += item.value; acc[key].count += 1; return acc; }, {}); }, [data]); return <Chart data={chartData} />; }

保持对象和数组的引用稳定

这个用法很容易被忽略,但在性能优化中很重要:

jsx
function Parent({ items }) { // ❌ 每次渲染创建新对象,子组件 React.memo 失效 const style = { color: "red" }; const config = { threshold: 0.5, rootMargin: "0px" }; // ✅ 对象引用稳定 const style = useMemo(() => ({ color: "red" }), []); const config = useMemo(() => ({ threshold: 0.5, rootMargin: "0px" }), []); return <Child style={style} observerConfig={config} />; }

useCallback 缓存函数,useMemo 缓存值——这是一个重要的互补关系。当你需要传一个稳定引用的对象或数组给子组件时,用 useMemo。

useMemo 和 useRef 的区别

面试中经常追加这个问题。两者都能"记住"上一次的值,但机制完全不同:

特征useMemouseRef
触发重渲染依赖变化时返回新值,组件正常重渲染修改 .current 不触发重渲染
用途缓存计算结果持久化可变值(DOM 引用、前一次渲染的值等)
依赖追踪有依赖数组,自动更新无依赖,手动管理
jsx
// useMemo:依赖变了才重算,结果参与渲染 const sorted = useMemo(() => data.sort(), [data]); // useRef:记住上一次的值,但不触发重渲染 const prevCount = useRef(count); useEffect(() => { prevCount.current = count; }, [count]);

如果你只是想跨渲染记住一个值但不影响渲染输出,用 useRef;如果你需要基于依赖缓存计算结果参与渲染,用 useMemo。

组合使用:性能优化三层架构

在一个复杂的列表组件中,三个 Hook 经常协同工作:

jsx
function SearchResults({ query, data }) { // 第一层:缓存数据计算结果 const results = useMemo(() => { return data.filter(item => item.name.toLowerCase().includes(query.toLowerCase()) ); }, [data, query]); // 第二层:缓存事件处理函数 const handleItemClick = useCallback((id) => { console.log("Selected:", id); }, []); // 第三层:缓存传递给子组件的 props 对象 const listProps = useMemo(() => ({ items: results, onItemClick: handleItemClick }), [results, handleItemClick]); return <ResultList {...listProps} />; }

这种"数据 → 函数 → props 对象"的三层缓存,是 React 性能优化的标准范式。但记住,这是在已经发现性能问题之后的优化手段,不是写代码时的默认操作。

用 DevTools 验证优化效果

说了这么多"什么时候该用",那怎么判断你写的 useCallback / useMemo 真的有用?靠猜是不行的,用 React DevTools Profiler:

  1. 打开 Chrome DevTools → Profiler 标签
  2. 点击录制按钮,操作你的组件
  3. 停止录制,查看火焰图
  4. 找到不必要的重渲染(灰色条表示"没变但重渲染了")
  5. 针对性地加 useCallback / useMemo,再录一次对比

如果你加了缓存但 Profiler 没有变化,说明这个缓存是多余的——移除它。优化从来不是越多越好。

三个最常见的坑

坑一:过度使用——简单运算不需要缓存

jsx
// ❌ 两个数相加也要 useMemo?缓存的成本比计算还大 const total = useMemo(() => a + b, [a, b]); // ❌ 简单的字符串拼接也要 useCallback? const label = useCallback(() => `${firstName} ${lastName}`, [firstName, lastName]); // ✅ 直接写 const total = a + b; const label = `${firstName} ${lastName}`;

经验法则:计算操作耗时 < 1ms 的,不需要 useMemo。只有循环遍历大数组、递归、复杂对象转换才值得。useCallback 同理——如果子组件没被 React.memo 包裹,你加不加 useCallback 效果一样。

坑二:闭包陷阱——遗漏依赖

jsx
// ❌ multiplier 在闭包里但不在依赖数组 const multiplier = 2; const result = useMemo(() => value * multiplier, [value]); // ✅ 所有引用的外部变量都要声明 const result = useMemo(() => value * multiplier, [value, multiplier]);

务必开启 eslint-plugin-react-hooksexhaustive-deps 规则,让 ESLint 帮你检查。闭包陷阱不是"偶尔遇到"的问题,是"迟早遇到"的问题。

坑三:useCallback 配合了没 memo 的子组件

jsx
// ❌ 子组件没有 React.memo,useCallback 基本白用 function Parent() { const handleClick = useCallback(() => {}, []); return <PlainChild onClick={handleClick} />; }

没有 React.memo 的子组件,父组件渲染它就会渲染——useCallback 改变不了这一点。这是一个非常常见的"写了等于没写"的场景。

React 19 Compiler 会取代它们吗?

React 19 引入了 React Compiler(实验性),可以自动为代码插入等效的 useMemouseCallback。如果你的项目已经启用了 Compiler,手动写这些 Hook 的需求会大幅减少。

但目前绝大多数项目(React 16-18)仍然需要手动优化。而且即使有了 Compiler,理解 useCallback 和 useMemo 的原理,能帮你在遇到性能问题时快速定位根因——Compiler 不是万能的,它也会犯错,这时候你需要知道底层的运作方式来判断是 Compiler 的 bug 还是你代码的问题。

面试速答模板

面试中被问到这道题,建议这样组织答案:

区别:useCallback 缓存函数引用,useMemo 缓存计算结果。useCallback(fn, deps) 等价于 useMemo(() => fn, deps),是它的语法糖。

场景:useCallback 主要配合 React.memo 阻止子组件无效渲染,或作为自定义 Hook 的稳定返回值;useMemo 用于昂贵计算和保持对象/数组引用稳定。

注意点:两者都不是"越多越好"。没有 React.memo 的子组件加 useCallback 无效;简单计算加 useMemo 反而更慢;务必开启 exhaustive-deps 规则避免闭包陷阱。

追问准备:React 19 Compiler 可以自动处理大部分缓存需求,但理解原理对定位问题仍然必要。useRef 也能"记住"值但不触发重渲染,和 useMemo 的触发机制不同。

总结

你需要用什么关键搭档
缓存函数给子组件useCallbackReact.memo
缓存计算结果useMemo
缓存对象/数组引用useMemoReact.memo
防止 useEffect 不必要触发useCallback / useMemo依赖数组

记住三点:

  1. 先写对,再优化。等 React DevTools Profiler 告诉你哪里慢了再动手,不要预先给所有东西加缓存
  2. useCallback 是 useMemo 的特例,缓存的是函数引用,不是计算结果
  3. 没有 React.memo 的子组件,useCallback 基本是自我安慰
标签:ReactReact Hook