React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React useMemo 与 useCallback原理

React useMemo 与 useCallback原理、场景与避坑完全指南

作者:卡布鲁

‌useMemo 缓存计算结果,useCallback 缓存函数引用,二者本质都是通过依赖数组控制缓存失效,避免不必要的重复计算和子组件重渲染,本文介绍React useMemo 与 useCallback原理、场景与避坑完全指南,感兴趣的朋友跟随小编一起看看吧

‌useMemo 缓存计算结果,useCallback 缓存函数引用,二者本质都是通过依赖数组控制缓存失效,避免不必要的重复计算和子组件重渲染。‌

这两个 Hook 是 React 性能优化的核心工具,但核心定位完全不同。React 函数组件每次渲染都会重新执行整个函数体,所有变量、函数都会重新创建,这正是它们要解决的问题。

核心原理

一、先搞懂一个问题:为什么需要记忆化?

在 React 中,每次组件重新渲染时,函数组件内部的所有变量和函数都会被重新创建。对于简单逻辑这毫无影响,但在以下场景中会带来性能问题:

useMemo 和 useCallback 就是 React 提供的两个记忆化(Memoization) Hook,核心思想一致:依赖不变,就复用上次结果,跳过重复工作。

二、useMemo:缓存计算结果

2.1 基本语法

const cachedValue = useMemo(() => {
  // 耗时计算逻辑
  return result;
}, [dep1, dep2]);

2.2 底层原理

要真正理解 useMemo,必须先了解 React 的 Fiber 架构和 Hook 链表机制。

2.2.1 数据存储:Hook 链表

在 React 的 Fiber 架构中,每个函数组件对应一个 Fiber 节点,该节点的 memoizedState 属性指向一个单向链表,按调用顺序串联组件中所有的 Hook 节点。

// Hook 节点的数据结构(简化版)
interface Hook {
  memoizedState: any,   // 存储当前 Hook 的状态值
  baseState: any,       // 基础状态
  baseQueue: any,       // 基础更新队列
  queue: any,           // 待处理更新队列
  next: Hook | null,    // 指向下一个 Hook
}

对于 useMemo,其 memoizedState 存储的是一个**数组 ****[value, deps]**,其中:

fiber.memoizedState → hook1(useState) → hook2(useMemo) → hook3(useEffect) → null
                       [count, null]    [cachedValue, deps]  [cleanup, deps]

💡 这就是为什么 Hook 必须在顶层调用、不能放在条件语句中——React 靠调用顺序来定位链表中的节点,一旦顺序变化,就会读取到错误的状态。

2.2.2 双缓存机制:Current 与 WorkInProgress

React 采用双 Fiber 树机制:

每次组件重新渲染时,React 会基于 Current Tree 克隆出 WorkInProgress Tree,在 WorkInProgress 上执行组件函数、调用 Hook,完成后通过指针交换(root.current = finishedWork)完成更新。

useMemo 的缓存读取正是利用了这一机制:

// 简化版:从 alternate(旧的 Current Fiber)读取上次的 Hook 状态
const oldHook = workInProgress.alternate?.memoizedState;
const cachedValue = oldHook?.memoizedState[0];  // 取出上次缓存的值
const cachedDeps = oldHook?.memoizedState[1];   // 取出上次的依赖

2.2.3 挂载阶段:mountMemo

组件首次渲染时,调用 mountMemo:

function mountMemo<T>(nextCreate: () => T, deps): T {
  // 1. 创建新的 Hook 节点,追加到 Hook 链表末尾
  const hook = mountWorkInProgressHook();
  // 2. 标准化依赖数组(undefined → null)
  const nextDeps = deps === undefined ? null : deps;
  // 3. 立即同步执行计算函数(在 Render 阶段)
  const nextValue = nextCreate();
  // 4. 将 [计算结果, 依赖数组] 存入 memoizedState
  hook.memoizedState = [nextValue, nextDeps];
  // 5. 返回计算结果供组件使用
  return nextValue;
}

关键点:挂载阶段没有历史缓存可比较,所以无条件执行计算函数,建立初始缓存记录。

2.2.4 更新阶段:updateMemo

组件重新渲染时,调用 updateMemo,这是缓存逻辑的核心:

function updateMemo<T>(nextCreate: () => T, deps): T {
  // 1. 从 Hook 链表中按顺序取出对应的 Hook 节点
  const hook = updateWorkInProgressHook();
  // 2. 标准化新依赖数组
  const nextDeps = deps === undefined ? null : deps;
  // 3. 读取上次缓存的状态 [cachedValue, cachedDeps]
  const prevState = hook.memoizedState;
  // 4. 依赖比较:如果依赖未变化,直接返回缓存值
  if (nextDeps !== null) {
    const prevDeps = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];  // 🎯 命中缓存,跳过计算
    }
  }
  // 5. 依赖变化了,重新执行计算
  const nextValue = nextCreate();
  // 6. 更新缓存
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

2.2.5 依赖比较算法:Object.is 浅比较

areHookInputsEqual 是决定缓存是否命中的关键函数:

function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) return false;
  // 长度不同,直接判定为变化
  if (nextDeps.length !== prevDeps.length) return false;
  // 逐项使用 Object.is 进行浅比较
  for (let i = 0; i < prevDeps.length; i++) {
    if (!Object.is(nextDeps[i], prevDeps[i])) {
      return false;
    }
  }
  return true;
}

React 使用 Object.is 而非 ===,主要差异在于:

这也解释了为什么引用类型作为依赖时必须保持引用稳定——即使对象内容完全相同,只要引用地址变了({} !== {}),Object.is 就会判定为"变化",触发重新计算。

2.2.6 执行时机:Render 阶段同步执行

useMemo 的计算函数在 Render 阶段同步执行,这意味着:

✅ 可以读取 props、state 等响应式数据

❌ 不能包含副作用(API 请求、DOM 操作、定时器等),因为 Render 阶段可能被 React 中断并重试

📌 副作用应该用 useEffect,它在 Commit 阶段异步执行,且保证只执行一次。

2.3 典型使用场景

✅ 场景一:昂贵计算缓存

function ProductList({ products, searchTerm }) {
  // 每次渲染都 filter 上千条数据 → 卡顿
  const filtered = useMemo(() => {
    return products.filter(p =>
      p.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [products, searchTerm]);  // 仅当这两个值变化时才重新筛选
  return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

✅ 场景二:稳定对象/数组引用,配合React.memo

function Parent({ user }) {
  // 每次渲染都创建新对象 → 子组件 memo 失效
  const config = useMemo(() => ({
    theme: 'dark',
    locale: user.locale,
    permissions: calcPermissions(user.role),
  }), [user.locale, user.role]);
  return <ChildComponent config={config} />;
}
const ChildComponent = React.memo(({ config }) => {
  // 只有 config 引用真正变化时才重渲染
  return <div>{config.theme}</div>;
});

✅ 场景三:避免 Effect 频繁触发

const options = useMemo(() => ({ method: 'GET', headers }), [headers]);
useEffect(() => {
  fetch(url, options);  // options 引用稳定,Effect 不会重复执行
}, [options]);

2.4 ❌ 不该用 useMemo 的场景

// ❌ 简单运算,缓存开销 > 计算开销
const value = useMemo(() => count + 1, [count]);
// ✅ 直接计算即可
const value = count + 1;
// ❌ 在 useMemo 中执行副作用
const data = useMemo(() => {
  fetchData();  // 副作用!
  return processedData;
}, [deps]);
// ✅ 副作用用 useEffect

三、useCallback:缓存函数引用

3.1 基本语法

const memoizedFn = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

3.2 底层原理

useCallback 的底层实现与 useMemo 几乎完全一致,甚至可以认为 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。 两者的核心差异仅在于存储的内容不同:

useMemouseCallback
memoizedState[0]计算函数的返回值回调函数本身(函数引用)
缓存目的避免重复计算保持引用稳定

3.2.1 挂载阶段:mountCallback

function mountCallback<T>(callback: T, deps): T {
  // 1. 创建新的 Hook 节点
  const hook = mountWorkInProgressHook();
  // 2. 标准化依赖数组
  const nextDeps = deps === undefined ? null : deps;
  // 3. 将 [函数引用, 依赖数组] 存入 memoizedState
  //    注意:这里存的是函数本身,不是执行结果
  hook.memoizedState = [callback, nextDeps];
  // 4. 返回原始函数
  return callback;
}

与 mountMemo 的关键区别:mountMemo 会执行 nextCreate() 获取返回值再存储,而 mountCallback 不执行函数,直接存储函数引用。

3.2.2 更新阶段:updateCallback

function updateCallback<T>(callback: T, deps): T {
  // 1. 从 Hook 链表中按顺序取出对应的 Hook 节点
  const hook = updateWorkInProgressHook();
  // 2. 标准化新依赖数组
  const nextDeps = deps === undefined ? null : deps;
  // 3. 读取上次缓存的 [函数引用, 依赖数组]
  const prevState = hook.memoizedState;
  // 4. 依赖比较:使用与 useMemo 完全相同的算法
  if (nextDeps !== null) {
    const prevDeps = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];  // 🎯 返回缓存的函数引用
    }
  }
  // 5. 依赖变化,更新缓存为新函数
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

3.2.3 为什么 useCallback 是 useMemo 的特例

从源码层面看,两者的核心逻辑完全一致,唯一的区别在于:

// useMemo:存储的是执行结果
hook.memoizedState = [nextCreate(), nextDeps];  // 先执行,再存结果
// useCallback:存储的是函数本身
hook.memoizedState = [callback, nextDeps];       // 不执行,直接存引用

这也是为什么官方提供两个独立 API 的原因:

3.2.4 只缓存引用,不缓存执行

这是 useCallback 最容易被误解的一点:

const handleClick = useCallback(() => {
  console.log('clicked', count);  // count 是闭包捕获的值
}, [count]);

3.2.5 为什么必须配合 React.memo

useCallback 单独使用没有任何性能收益,它只是让函数引用不变,但函数引用不变本身不会减少任何计算。只有当这个稳定的引用传递给用 React.memo 包裹的子组件时,才能避免子组件因 props 引用变化而无效重渲染。

父组件渲染
    ↓
useCallback 返回同一个函数引用
    ↓
React.memo 对子组件 props 做浅比较
    ↓
引用没变 → 跳过子组件渲染 ✅
引用变了 → 子组件重渲染 ❌

💡 关键区别:useCallback 只缓存函数引用,不缓存执行结果。函数体内部逻辑每次调用时仍然会执行,只是引用地址不变。

3.3 典型使用场景

✅ 场景一:传递给React.memo子组件的回调

// 父组件
function Parent() {
  const [count, setCount] = useState(0);
  // 不用 useCallback → 每次渲染创建新函数 → Child 重渲染
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);  // 空依赖,整个生命周期引用不变
  return (
    <>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
    </>
  );
}
// 子组件
const Child = React.memo(({ onClick }) => {
  console.log('Child rendered');  // 只有 onClick 引用变化时才打印
  return <button onClick={onClick}>Click</button>;
});

✅ 场景二:函数作为其他 Hook 的依赖

const fetchUser = useCallback((id) => {
  return api.get(`/users/${id}`);
}, []);
useEffect(() => {
  fetchUser(userId);  // fetchUser 引用稳定,Effect 只依赖 userId
}, [fetchUser, userId]);

✅ 场景三:高频事件处理

const handleScroll = useCallback((e) => {
  setScrollY(e.target.scrollTop);
}, []);
// 配合节流/防抖时,稳定的引用很重要
useEffect(() => {
  const throttled = throttle(handleScroll, 100);
  window.addEventListener('scroll', throttled);
  return () => window.removeEventListener('scroll', throttled);
}, [handleScroll]);

3.4 ❌ 不该用 useCallback 的场景

// ❌ 仅组件内部使用、不传递给子组件的函数
const handleClick = useCallback(() => { setCount(c => c + 1); }, []);
// ✅ 直接写即可,没有优化收益
const handleClick = () => { setCount(c => c + 1); };
// ❌ 子组件没有用 React.memo 包裹
// useCallback 单独使用毫无意义,必须配合 memo 才能拦截重渲染

四、useMemo vs useCallback 核心对比

维度useMemouseCallback
缓存对象计算结果(值)函数引用(方法)
执行时机依赖变化时自动执行计算仅缓存引用,手动调用才执行
解决的问题避免重复 CPU 计算避免函数引用变更导致子组件无效重渲染
能否单独使用✅ 可以,直接减少计算量❌ 单独使用无意义,需配合 React.memo
等价关系useMemo(() => fn, deps) ≡ useCallback(fn, deps)—

五、黄金搭档:React.memo + useMemo + useCallback

三者形成完整的缓存链路

React.memo    → 拦截组件重渲染(组件级缓存)
useMemo       → 稳定数据引用(值级缓存)
useCallback   → 稳定函数引用(函数级缓存)
// 完整示例:带搜索和选择的商品列表
const ProductItem = React.memo(({ product, onSelect }) => {
  return (
    <li onClick={() => onSelect(product.id)}>
      {product.name} - ¥{product.price}
    </li>
  );
});
function ProductPage({ products, keyword }) {
  const [selected, setSelected] = useState(null);
  // 1. useMemo 缓存筛选结果,避免每次渲染都遍历
  const filtered = useMemo(() => {
    const lower = keyword.toLowerCase();
    return products.filter(p => p.name.toLowerCase().includes(lower));
  }, [products, keyword]);
  // 2. useCallback 稳定回调引用,配合 React.memo 生效
  const handleSelect = useCallback((id) => {
    setSelected(id);
  }, []);
  return (
    <ul>
      {filtered.map(p => (
        <ProductItem key={p.id} product={p} onSelect={handleSelect} />
      ))}
    </ul>
  );
}

六、避坑指南

坑 1:依赖数组写漏 → 拿到过期数据

// ❌ 忘记把 filter 加入依赖
const filtered = useMemo(() =>
  items.filter(i => i.category === filter), [items]);
// ✅ 所有响应式变量都要加入依赖
const filtered = useMemo(() =>
  items.filter(i => i.category === filter), [items, filter]);

坑 2:依赖数组写死[]→ 缓存永不更新

// ❌ 依赖写空数组,total 永远是初始值
const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), []);

坑 3:闭包陷阱 → 函数捕获过期状态

// ❌ 依赖为空,count 永远是初始值 0
const increment = useCallback(() => {
  setCount(count + 1);
}, []);
// ✅ 使用函数式更新
const increment = useCallback(() => {
  setCount(prev => prev + 1);
}, []);

坑 4:过度使用 → 反而更慢

useMemo / useCallback 本身有成本:维护缓存占用内存 + 每次渲染做依赖比较。如果计算本身很快(1ms 内),缓存开销反而大于计算开销。

📌 黄金法则:先用 React DevTools Profiler 定位瓶颈,确认存在性能问题后再加记忆化,不要"以防万一"到处套。

七、前瞻:React 19 编译器自动记忆化

React 19 引入了React Compiler(原 React Forget),能够自动对组件中的值和函数进行记忆化处理,大幅减少手动调用 useMemo / useCallback 的需求。

// React 19 + Compiler 开启后,以下代码会被自动优化
function Component({ items }) {
  const total = items.reduce((s, i) => s + i.price, 0);  // 自动 memo
  const handleClick = () => { console.log('click'); };    // 自动 callback
  return <div>{total}</div>;
}

但理解 useMemo 和 useCallback 的底层原理仍然非常重要——编译器并非万能,在复杂场景下你仍需手动干预,且面试和代码审查中这些概念依然是高频考点。

八、总结速查表

场景用哪个
大数据筛选/排序/聚合useMemo
复杂数学计算、递归useMemo
稳定对象/数组引用传给子组件useMemo
函数传给 React.memo 子组件useCallback
函数作为 useEffect 等 Hook 的依赖useCallback
简单运算、组件内部自用函数都不用
不确定是否有性能问题先 Profiler,再决定

到此这篇关于React useMemo 与 useCallback原理、场景与避坑完全指南的文章就介绍到这了,更多相关React useMemo 与 useCallback原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文