React useMemo 源码ai解析

React useMemo 源码深度解析

前言

useMemo 是 React Hooks 中用于性能优化的重要工具,它可以缓存计算结果,避免在每次渲染时重复执行昂贵的计算。本文将深入 React 源码,探索 useMemo 的实现原理。

使用示例

在深入源码之前,先回顾一下 useMemo 的基本用法:

javascript 复制代码
const memoizedValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

useMemo 接收两个参数:

  • 第一个参数是创建函数(create function),返回需要缓存的值
  • 第二个参数是依赖数组,只有依赖项发生变化时才会重新计算

源码位置

useMemo 的源码主要分布在以下文件中:

  • packages/react/src/ReactHooks.js - 导出的 API 入口
  • packages/react-reconciler/src/ReactFiberHooks.js - 核心实现逻辑

入口实现

ReactHooks.js 中,useMemo 只是一个简单的代理:

javascript 复制代码
export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}

这里的 resolveDispatcher() 会根据当前组件的渲染阶段返回不同的 dispatcher:

  • mount 阶段 (首次渲染):HooksDispatcherOnMount
  • update 阶段 (更新渲染):HooksDispatcherOnUpdate

Mount 阶段:mountMemo

首次渲染时调用 mountMemo

javascript 复制代码
function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 创建一个新的 Hook 对象
  const hook = mountWorkInProgressHook();
  
  const nextDeps = deps === undefined ? null : deps;
  
  // 执行创建函数,计算初始值
  const nextValue = nextCreate();
  
  // 将值和依赖保存到 hook.memoizedState
  hook.memoizedState = [nextValue, nextDeps];
  
  return nextValue;
}

关键点解析

  1. mountWorkInProgressHook():在 fiber 节点上创建一个新的 Hook 对象,并将其添加到 Hook 链表中
  2. hook.memoizedState :存储了一个数组 [value, deps]
    • value:缓存的计算结果
    • deps:依赖数组
  3. 首次渲染必定执行nextCreate() 会被调用,计算初始值

Update 阶段:updateMemo

组件更新时调用 updateMemo

javascript 复制代码
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 获取当前 Hook 对象
  const hook = updateWorkInProgressHook();
  
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      
      // 比较新旧依赖是否相同
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 依赖未变化,返回缓存的值
        return prevState[0];
      }
    }
  }
  
  // 依赖变化,重新计算
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  
  return nextValue;
}

关键点解析

  1. updateWorkInProgressHook():从当前 fiber 的 Hook 链表中获取对应的 Hook 对象
  2. areHookInputsEqual() :逐个比较依赖项,使用 Object.is() 进行浅比较
  3. 短路优化 :如果依赖未变化,直接返回 prevState[0],不执行 nextCreate()

依赖比较:areHookInputsEqual

这是 useMemo 性能优化的核心:

javascript 复制代码
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  if (prevDeps === null) {
    return false;
  }

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // 使用 Object.is 进行比较
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

Object.is vs ===

Object.is()=== 的主要区别:

  • Object.is(+0, -0) 返回 false,而 +0 === -0 返回 true
  • Object.is(NaN, NaN) 返回 true,而 NaN === NaN 返回 false

在 React 中,is()Object.is 的 polyfill 实现。

Hook 数据结构

每个 Hook 在内部都是一个链表节点:

javascript 复制代码
type Hook = {
  memoizedState: any,        // 保存的状态(useMemo 中是 [value, deps])
  baseState: any,            // 基础状态
  baseQueue: Update<any>,    // 待处理的更新队列
  queue: UpdateQueue<any>,   // 当前更新队列
  next: Hook | null,         // 下一个 Hook
};

对于 useMemo

  • memoizedState 存储 [cachedValue, dependencies]
  • next 指向下一个 Hook(如 useStateuseEffect 等)

与 useCallback 的关系

useCallback 实际上是 useMemo 的特殊形式:

javascript 复制代码
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  
  // 直接保存函数本身,而不是执行它
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

可以理解为:

javascript 复制代码
useCallback(fn, deps) 
// 等价于
useMemo(() => fn, deps)

使用注意事项

1. 依赖数组必须完整

javascript 复制代码
// ❌ 错误:缺少依赖
const result = useMemo(() => a + b, [a]);

// ✅ 正确:包含所有依赖
const result = useMemo(() => a + b, [a, b]);

2. 依赖是浅比较

javascript 复制代码
// ❌ 每次都会重新计算(对象引用不同)
const result = useMemo(() => compute(obj), [{ id: 1 }]);

// ✅ 使用稳定的引用
const obj = { id: 1 };
const result = useMemo(() => compute(obj), [obj]);

3. 不要过度使用

useMemo 本身也有开销(保存依赖、比较依赖),只在以下情况使用:

  • 计算成本高
  • 返回值是对象/数组,用于其他 Hook 的依赖
  • 渲染频繁且计算昂贵
javascript 复制代码
// ❌ 过度优化
const doubled = useMemo(() => count * 2, [count]);

// ✅ 简单计算直接写
const doubled = count * 2;

4. 空依赖数组 vs 无依赖数组

javascript 复制代码
// 只在 mount 时计算一次
const value1 = useMemo(() => expensive(), []);

// 每次渲染都重新计算(失去了 useMemo 的意义)
const value2 = useMemo(() => expensive());

性能对比

通过一个简单的性能测试:

javascript 复制代码
// 不使用 useMemo
function Component({ items }) {
  const filtered = items.filter(item => item.active);
  return <List data={filtered} />;
}

// 使用 useMemo
function Component({ items }) {
  const filtered = useMemo(
    () => items.filter(item => item.active),
    [items]
  );
  return <List data={filtered} />;
}

items 未变化时:

  • 不使用 useMemo :每次渲染都执行 filter,并创建新数组(可能导致 List 子组件重渲染)
  • 使用 useMemo:返回缓存的数组,避免不必要的计算和子组件渲染

源码调试技巧

如果想在本地调试 React 源码:

  1. Clone React 仓库:
bash 复制代码
git clone https://github.com/facebook/react.git
cd react
  1. 构建 dev 版本:
bash 复制代码
yarn
yarn build react/index,react-dom/index --type=NODE
  1. 在你的项目中链接本地 React:
bash 复制代码
cd your-project
npm link ../react/build/node_modules/react
npm link ../react/build/node_modules/react-dom
  1. ReactFiberHooks.js 中添加 console.logdebugger 语句

总结

useMemo 的核心原理:

  1. 数据结构 :使用 [value, deps] 存储缓存值和依赖
  2. 比较策略 :使用 Object.is 浅比较依赖数组
  3. 两阶段实现
    • Mount:直接执行并缓存
    • Update:先比较依赖,命中则返回缓存,否则重新计算
  4. Hook 链表 :通过 next 指针维护多个 Hook 的调用顺序

理解 useMemo 的源码实现,可以帮助我们:

  • 正确使用依赖数组
  • 避免过度优化
  • 理解 React Hooks 的底层机制
  • 更好地进行性能调优

参考资料


最后更新:2026-03-17

相关推荐
张一凡932 小时前
easy-model 在任务管理应用中的实际应用
前端·react.js
Tzarevich2 小时前
深入理解Event Loop:从原理图到代码实战,小白也能看懂的 JS 执行机制
前端·javascript·面试
毛骗导演2 小时前
发送一句「你好」,为什么花掉了几千个 Token?——深读 OpenClaw 的 Context 注入机制
前端·架构
工边页字2 小时前
AI产品中的长期记忆和短期记忆是什么,你知道吗?
前端·人工智能·后端
HelloReader2 小时前
Flutter 页面导航Navigator.push 与自适应导航模式(十四)
前端
小凡同志2 小时前
那个复制粘贴了二十次 loading 的下午
前端·vue.js
HelloReader2 小时前
Flutter 底层原理揭秘框架如何工作(十五)
前端
南篱2 小时前
前端必看:一口气搞懂跨域是什么、为什么、怎么解决
前端·javascript·面试
qq_406176142 小时前
Vue 插槽与组件传参:从入门到精通
前端·javascript·vue.js