陌上人如玉
公子世无双

2.7 useMemo 与 useCallback

2.7 useMemo 与 useCallback

一句话:两者都是「引用稳定工具」——useMemo 缓存计算结果,useCallback 缓存函数引用;它们的价值只在「配合 React.memo」或「计算确实昂贵」时才成立,滥用反而增加成本。

一、基本用法

// useMemo:缓存「计算的结果」
const sorted = useMemo(() => {
  return [...list].sort((a, b) => a.order - b.order);   // 只有 list 变化时才重算
}, [list]);

// useCallback:缓存「函数本身」(等价于 useMemo 返回函数)
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

// ⭐ 两者的关系:useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
const handleClick2 = useMemo(() => () => doSomething(id), [id]);
Hook 缓存对象 返回值 使用场景
useMemo 计算结果(任意值) 计算结果 昂贵计算、稳定对象/数组引用
useCallback 函数本身 函数 稳定函数引用(给 memo 子组件 / 作为依赖)

二、两者解决的核心问题:引用稳定

// 🎯 问题:每次渲染都是新的引用
function Parent() {
  const [count, setCount] = useState(0);

  // 每次渲染都会新建:新的对象、新的数组、新的函数
  const options = { page: 1, size: 10 };
  const list = [1, 2, 3];
  const handleClick = () => console.log('click');

  return (
    <>
      
      
      {/* ⚠️ 三个 props 全是新引用 → React.memo 的浅比较全部失败 → 子组件每次都重渲染 */}
    
  );
}

const MemoChild = React.memo(function Child({ options, list, onClick }: Props) {
  console.log('Child 渲染');
  return 
{list.length}
; }); // ✅ 稳定引用 function Parent2() { const [count, setCount] = useState(0); const options = useMemo(() => ({ page: 1, size: 10 }), []); // 常量可提到模块作用域 const list = useMemo(() => [1, 2, 3], []); const handleClick = useCallback(() => console.log('click'), []); return ( <> {/* ✅ props 引用稳定 → React.memo 生效 → 子组件不再无谓重渲染 */} ); }

注意:如果 options 是纯常量(不依赖任何值),其实应该提到组件外部(模块作用域),这样根本不需要 useMemo:

const OPTIONS = { page: 1, size: 10 };   // ✅ 模块级常量,引用天然稳定

三、什么时候真正有用(三个场景)

// ① 配合 React.memo 稳定 props 引用
const Child = React.memo(ExpensiveChild);

function Parent({ items, onSelect }) {
  const sorted = useMemo(() => [...items].sort(), [items]);
  const handleSelect = useCallback((id) => onSelect(id), [onSelect]);

  return ;
}

// ② 计算确实昂贵(避免每次渲染重算)
const stats = useMemo(() => {
  return expensiveAggregate(hugeList);       // 例:10 万条数据的统计
}, [hugeList]);

// ③ 作为其它 Hook 的依赖(保持引用稳定,避免 Effect 反复执行)
const config = useMemo(() => ({ url, method }), [url, method]);

useEffect(() => {
  fetchData(config);
}, [config]);          // ✅ config 引用稳定 → 只有 url/method 变化时才重新请求
// ⚠️ 如果不 memo,config 每次渲染都是新对象 → Effect 每次都执行 → 可能无限循环
场景 该不该用
值是常量(不依赖任何响应式值) ❌ 提到模块作用域更好
计算非常轻(a + b、简单 map) ❌ 不值得
结果要给 memo 子组件作 props ✅ 该用
计算昂贵(大数组排序/过滤/聚合) ✅ 该用
作为 useEffect/useMemo 的依赖 ✅ 该用
一次性的简单函数给原生元素(<button onClick>) ❌ 不需要
useMemo 里做副作用 ❌ 不应该(应放 Effect)

四、成本分析:为什么可能是负担

useMemo / useCallback 并不是「免费的」:

① 内存成本:React 要保存上一次的值与依赖数组
② 执行成本:每次渲染都要【比较依赖数组】(逐项 Object.is)
③ 代码成本:依赖数组要维护正确(漏了会出错,多了会失效)
④ 可读成本:包一层后逻辑变长,尤其嵌套时
⑤ 失效成本:写错的 useMemo 会「看起来在优化」,实际毫无作用(props 依然不稳定)

所以「无脑加 useMemo/useCallback」的结果常常是:
  代码更复杂、性能没提升、还引入了隐蔽 bug。
// ❌ 典型的「无效优化」
function Bad({ items }) {
  // 计算很轻,包一层反而更贵
  const count = useMemo(() => items.length, [items]);

  // 传给原生元素,不需要稳定引用(原生元素不是 memo 组件)
  const onClick = useCallback(() => console.log('ok'), []);

  return ;
}

// ✅ 直接写就好
function Good({ items }) {
  return ;
}

五、常见误用

// ① 依赖数组漏了 → 结果是「过期的」
const sorted = useMemo(() => [...list].sort(cmp), []);    // ❌ 应该依赖 [list]
// → 表现:list 变化了但 sorted 不变

// ② 依赖数组多写了「每次渲染都新建的东西」→ 完全失效
const value = useMemo(() => compute(a), [a, { extra: 1 }]);   // ❌ 第二个依赖每次都是新对象
// → 表现:每次都重新计算(等于没 memo)

// ③ 用 useMemo 缓存「本来就不稳定」的结果
const fn = useMemo(() => (x) => x + a, [a]);   // ✅ 这个是对的
const obj = useMemo(() => ({ a, b }), [a, b]); // ✅ 对

// ④ 在 useMemo 里做副作用(不应该)
const data = useMemo(() => {
  localStorage.setItem('x', 'y');              // ❌ 副作用不该在 useMemo 里
  return compute();
}, []);

// ⑤ useCallback 包裹的函数内部依赖了未声明的值
const handle = useCallback(() => {
  console.log(user.name);                      // ⚠️ user 没在依赖里 → 拿到旧值
}, []);

// ⑥ 给「每次渲染都要更新的值」做 memo → 依赖总是变化 → 无效
const dynamic = useMemo(() => ({ now: Date.now() }), [Date.now()]);   // ❌ 每次都变

// ⑦ 用 useMemo 替代「状态派生」的正确写法(其实应该直接算)
// 如果计算很轻,直接 const x = derive(a, b) 就行
// ⑦ 的示例:轻量派生不需要 useMemo
function Bad({ firstName, lastName }) {
  const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
  // ❌ 字符串拼接极快,memo 的成本 > 收益
  return 
{fullName}
; } function Good({ firstName, lastName }) { const fullName = `${firstName} ${lastName}`; // ✅ 直接算 return
{fullName}
; }

六、依赖数组的写法规范

// ① 依赖「用到的每一个响应式值」
const value = useMemo(() => compute(a, b), [a, b]);

// ② props 对象依赖「具体字段」
const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), [items]);
// 如果父组件能保证 items 引用稳定,就依赖 items;
// 否则依赖「能稳定比对的原始值」(如 items.length、items.map(i => i.id).join())

// ③ 函数依赖(层层下钻会很麻烦)
const handle = useCallback(() => onSelect(id), [onSelect, id]);

// ④ 需要「稳定但读最新值」时结合 useRef
const latestOnSelect = useRef(onSelect);
useEffect(() => { latestOnSelect.current = onSelect; });

const handleStable = useCallback((id: string) => {
  latestOnSelect.current(id);        // 引用稳定,但调用的是最新的 onSelect
}, []);                              // ⭐ 依赖为空,引用永不变化

七、与 React.memo 的关系

三者的关系(必须一起理解):

React.memo         →  浅比较 props,相同则跳过子组件重渲染
      ↑
      │ 需要 props 引用稳定
      │
useMemo            →  稳定「值」的引用(对象/数组/计算结果)
useCallback        →  稳定「函数」的引用

所以:
· 只用 React.memo,不配合 useMemo/useCallback → 大概率失效(props 每次都是新引用)
· 只用 useMemo/useCallback,子组件不是 memo → 没有意义(子组件本来就每次渲染)
· 三者配合才有意义,但也不能无脑加 → 判断标准是「渲染成本是否真的高」
// 完整的配合示例
const Row = React.memo(function Row({ item, onDelete }: RowProps) {
  console.log('Row 渲染', item.id);
  return (
    
  • {item.name} {/* ⚠️ 这里的箭头函数是「Row 内部的」,不影响 Row 自身的 memo */}
  • ); }); function List({ items, onDelete }: { items: Item[]; onDelete: (id: string) => void }) { // 稳定的处理函数(如果 onDelete 本身稳定) const handleDelete = useCallback((id: string) => onDelete(id), [onDelete]); return (
      {items.map((it) => ( ))}
    ); } // ✅ 只要 item 与 handleDelete 引用稳定,Row 就不会重渲染

    八、常见坑

    // ① 用 useMemo 包「廉价计算」
    const doubled = useMemo(() => count * 2, [count]);       // ❌ 不值得
    
    // ② 依赖数组漏值 → 结果过期
    const filtered = useMemo(() => list.filter(fn), []);     // ❌
    
    // ③ 依赖数组里有新对象 → 完全失效
    const v = useMemo(() => f(a), [a, {}]);                   // ❌
    
    // ④ useCallback 里用了未声明的依赖
    const h = useCallback(() => g(user), []);                 // ❌ user 会是旧值
    
    // ⑤ 在 useMemo 里写副作用
    useMemo(() => { track(); return 1; }, []);                // ❌
    
    // ⑥ 给「常量」做 memo(应该提到组件外)
    const OPTIONS = useMemo(() => ({ a: 1 }), []);            // ⚠️ 模块级常量更好
    
    // ⑦ 把所有回调都 useCallback → 代码噪音大,收益为零
    // ⑧ 「为了消除 eslint 警告」而把函数用 useCallback 包起来,但依赖写错
    // ⑨ 认为 useMemo 能「保证」不重算(React 保留丢弃缓存的权利)
    // ⑩ 在 useMemo 的依赖里用「引用会变但内容相同」的对象 → 无效 memo
    // ⑪ 用 useMemo 记忆「带副作用的方法调用」
    // ⑫ 忘记「子组件不是 memo 时,父组件的 useCallback 毫无意义」
    

    面试延伸

    1. 「useMemo 和 useCallback 有什么区别?」
      两者都是「引用稳定工具」,区别在缓存的对象不同:useMemo 缓存计算结果(任意值),useCallback 缓存函数本身。它们本质上是同一个机制——useCallback(fn, deps) 完全等价于 useMemo(() => fn, deps),所以 useCallback 只是 useMemo 的语法糖。

    2. 「useMemo/useCallback 真的能优化性能吗?」
      分场景:① 配合 React.memo —— 能(稳定 props 引用让 memo 生效,跳过子组件重渲染);② 昂贵计算 —— 能(避免每次渲染重算);③ 作为其它 Hook 的依赖 —— 能(避免 Effect 反复执行);④ 其它情况 —— 通常不能甚至变负(比较依赖、保存缓存、维护依赖数组都有成本)。所以标准做法是「先用 Profiler 确认这里是瓶颈,再加」。

    3. 「为什么加了 useMemo 却没有效果?」
      三个最常见原因:① 依赖数组里有「每次渲染都新建的值」(对象/数组/内联函数字面量)→ 依赖永远「变了」→ 每次都重算;② 子组件不是 React.memo 包裹的 → 父组件重渲染时子组件本来就会重新执行,props 稳定与否没有意义;③ 子组件是 memo,但还有别的 props 不稳定 → 只要有一个 props 引用变化,浅比较就失败,其余 memo 都白做。

    4. 「useMemo 能保证计算只执行一次吗?」
      不能。React 官方文档明确说「useMemo 是性能优化手段,不是语义保证」——React 保留在必要时丢弃缓存的权利(例如为了释放内存、或在未来的并发特性下)。所以不要把「只执行一次」的逻辑放在 useMemo 里(如初始化只该执行一次的副作用),那种场景应该用 useRef 的延迟初始化或 useState 的惰性初始化。

    5. 「什么时候不该用 useMemo/useCallback?」
      ① 计算很轻(简单算术、字符串拼接、小数组 map);② 常量(应该提到模块作用域);③ 传给原生 DOM 元素的事件(原生元素不是 memo 组件,不需要稳定引用);④ 子组件不是 memo 的;⑤ 为了「消除 lint 警告」而机械地包一层(这会让依赖维护变复杂)。判断标准是「是否真的减少了工作」。

    6. 「useCallback 的依赖怎么写才不会一直变?」
      常见问题:依赖里有「父组件传下来的内联函数」或「每次渲染新建的对象」,导致 useCallback 的引用也一直变。解法:① 让上游也稳定(层层 useCallback/useMemo);② 依赖「原始值」而不是对象;③ 需要「稳定但读最新值」时用 useRef 保存最新值 + useCallback(..., [])(引用永不变化);④ 把处理函数做成「用 data-* 传参」的稳定函数。

    7. 「useMemo 和 useCallback 与 React Compiler 的关系?」
      React Compiler(React 19 的实验特性)会自动在需要的地方插入记忆化(相当于自动帮你写 useMemo/useCallback/memo),并且比手写更精确。所以长期看「手动优化」的价值会下降。但目前:① Compiler 还未全面落地;② 手动优化的思路(理解引用稳定、减少重渲染)依然重要——因为 Compiler 也有覆盖不到的地方(如跨组件的引用传递),而且「知道什么时候该优化」是性能工作的基础。

    8. 「如何判断一个 useMemo 是否值得保留?」
      三个检查项:① 依赖数组是否稳定(如果依赖里每次都是新引用,它完全没作用,应该先修依赖或删掉它);② 计算结果是否被 memo 子组件或其它 Hook 使用(如果只是给原生元素或只用于当前组件的渲染,通常不值得);③ 计算是否真的昂贵(用 Profiler 看该函数的耗时占比,占比很低就不值得)。不符合以上三条的 useMemo 应该删掉——删除它们往往能让代码更简单且性能不降。

    一句话速记

    useMemo 缓存结果、useCallback 缓存函数(后者是前者的语法糖);它们的本质是「引用稳定」,价值只在「配合 React.memo」「计算昂贵」「作为其它 Hook 的依赖」时成立;依赖里有新对象/新函数会完全失效(先修依赖);常量的 memo 应该提到模块作用域、轻量计算不值得 memo;useMemo 不是语义保证(React 可丢弃缓存),别把「只执行一次」的逻辑放进去。

    赞(0) 打赏
    未经允许不得转载:陌上寒 » 2.7 useMemo 与 useCallback

    评论 抢沙发

    觉得文章有用就打赏一下文章作者

    非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

    微信扫一扫

    支付宝扫一扫