先记住这个答案
useMemo 接收计算函数,需要时执行它并缓存返回值;useCallback 接收函数并返回可复用的函数引用,不会替你执行该业务函数。二者都依赖 Object.is 比较的依赖数组,并要求声明读取的响应式值。需要复用昂贵派生结果或稳定对象时考虑 useMemo,需要让 memo 子组件接收稳定回调时考虑 useCallback。它们都不是请求结果缓存,也不会自动修复闭包中的旧数据。
- useMemo 缓存计算返回值
- useCallback 缓存交出去的函数身份
- 调用时使用什么数据由闭包与参数决定
useCallback 不会帮你调用函数
把箭头函数传给 useCallback 时,JavaScript 仍会求值得到这一轮传入的函数;React 可以返回之前缓存的函数引用。它减少的是下游看到的身份变化,并不是禁止本轮创建函数表达式。
从用途上可将 useCallback 理解为缓存函数本身,而 useMemo 可以通过返回函数达到类似效果。不过计算函数会在渲染期间执行,若误把实际提交操作写成 useMemo 的计算,就可能在用户点击之前触发业务行为。
按派生值和业务回调分别声明依赖
示例先根据 items 求和,再让 submit 捕获 total、currency 和 onPay。无关计数不影响这三项,因此确认按钮可获得稳定回调;切换币种时,即使金额相同,也需要新回调传递新币种。
如果替换条目数组但合计数字相同,求和会重新进行,submit 却仍可能复用,因为它依赖的是合计值而不是整个数组。这种设计成立的前提是提交业务只需要金额和币种,不需要逐条商品信息。
import { memo, useCallback, useMemo, useState } from 'react';
type Line = Readonly<{ price: number; quantity: number }>;
const Confirm = memo(function Confirm({ total, onSubmit }: {
total: number;
onSubmit: () => void;
}) {
return <button onClick={onSubmit}>确认金额 {total}</button>;
});
export default function QuoteDemo({ items, currency, onPay }: {
items: readonly Line[];
currency: string;
onPay: (total: number, currency: string) => void;
}) {
const [revision, setRevision] = useState(0);
const total = useMemo(
() => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
[items]
);
const submit = useCallback(() => onPay(total, currency), [total, currency, onPay]);
return <>
<button onClick={() => setRevision(n => n + 1)}>刷新报价外层</button>
<output>报价计数:{revision}</output>
<Confirm total={total} onSubmit={submit} />
</>;
}金额示例仅演示缓存依赖,实际货币系统应明确最小单位与精度策略。测试要确认没有自动调用 onPay,并在点击、变更币种和替换外部处理函数后验证实际参数。
稳定函数并不等于自动读取最新值
给 submit 写空数组,会保留初次缓存函数捕获的输入,而不会让 total 或 currency 在调用时神奇地变成最新。应补全依赖,或者让调用者通过参数传入需要的数据;重构后仍要核对数据所有权。
缓存一个异步函数也不等于缓存它返回的 Promise。每次调用仍可能发起新请求,竞态、取消、去重和过期策略需要由请求层处理。把函数身份稳定与网络结果复用混在一起,会遗漏真正的数据一致性问题。
容易答错的地方
- 所有事件回调都套 useCallback
- 没有接收方依赖函数身份、也没有其他明确缓存收益时,包装会增加依赖维护而不一定减少工作。普通按钮的简单事件处理可以直接定义,确认下游比较或订阅确实需要稳定后再优化。
- 用 useMemo 发起请求并期待只请求一次
- 计算函数属于渲染阶段,可能重试且没有相应请求清理协议。数据请求应采用框架数据机制或可管理生命周期的方案,明确缓存键和取消策略,不能用依赖数组代替请求系统。
面试官还会怎么问?
useMemo 可以返回一个函数吗?
可以,返回值本身没有被限制为对象或数字。但如果目的是缓存函数身份,useCallback 通常更直接;无论写法如何,函数捕获的响应式输入都要被依赖描述,换一种 Hook 不会消除闭包问题。
函数内部用了函数式 setState 就能写空依赖吗?
只针对由更新函数参数提供的那份前态,可以减少直接读取旧 state 的需要。如果函数还读取当前 prop、币种或其他状态,它们仍需列入依赖;函数式更新不是省略全部依赖的通行证。
稳定回调能保证子组件不重新渲染吗?
不能单独保证。子组件还可能收到变化的其他 prop,或者因本地状态、Context 更新而执行;没有相应的复用边界时,稳定一个函数引用也不等于自动阻止父级带来的子组件渲染。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。