useMemo 实现原理

useMemo 实现原理深度解析

useMemo 是 React Hooks 中用于性能优化的重要工具,它的实现涉及到 React 的 Fiber 架构、Hooks 机制和渲染流程。

核心实现机制

useMemo 的核心思想是记忆化(Memoization):在依赖项未变化时返回缓存的值,避免重复计算。

1. 在 React 源码中的基本结构

在 React 源码中,useMemo 的实现大致如下:

javascript 复制代码
function useMemo(create, deps) {
  // 1. 获取当前正在渲染的 Fiber 节点和 Hook 链表
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}

实际的实现位于 ReactFiberHooks.js 中:

javascript 复制代码
function updateMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 2. 获取当前 Hook 对象
  const hook = updateWorkInProgressHook();
  
  // 3. 获取上一次的依赖项和记忆值
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null) {
    if (nextDeps !== null) {
      // 4. 比较依赖项是否变化
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 5. 依赖未变化,返回缓存的值
        return prevState[0];
      }
    }
  }
  
  // 6. 依赖变化或首次渲染,重新计算值
  const nextValue = create();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

Hook 在 Fiber 架构中的存储

要理解 useMemo,需要先了解 Hooks 在 React Fiber 架构中的存储方式:

1. Hook 链表结构

javascript 复制代码
// Fiber 节点中的 Hook 存储
type Fiber = {
  memoizedState: any, // 指向当前 Hook 链表的头部
  // ... 其他属性
};

// Hook 对象结构
type Hook = {
  memoizedState: any,     // 存储记忆的值
  baseState: any,         // 基础状态
  baseQueue: any,         // 基础队列
  queue: any,             // 更新队列
  next: Hook | null,      // 指向下一个 Hook
};

对于 useMemomemoizedState 字段存储的是一个数组 [value, deps]

  • value: 记忆的计算结果
  • deps: 上一次的依赖项数组

2. Hooks 的调用顺序规则

React 依赖 Hook 的调用顺序来正确关联状态:

javascript 复制代码
function MyComponent() {
  const [state] = useState(0);      // Hook 1
  const memoizedValue = useMemo(() => { // Hook 2
    return expensiveCalculation(state);
  }, [state]);
  const [anotherState] = useState(''); // Hook 3
  
  // React 通过调用顺序维护 Hook 链表:
  // Hook1 -> Hook2 -> Hook3
}

依赖比较的实现

useMemo 的核心在于依赖比较,React 使用 areHookInputsEqual 函数:

javascript 复制代码
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  // 处理边界情况
  if (prevDeps === null) {
    return false;
  }
  
  // 比较数组长度
  if (nextDeps.length !== prevDeps.length) {
    console.error(
      'useMemo received a different number of dependencies. ' +
      'Previous: %s, current: %s',
      prevDeps.length,
      nextDeps.length,
    );
    return false;
  }
  
  // 逐个比较依赖项
  for (let i = 0; i < prevDeps.length; i++) {
    // 使用 Object.is 进行严格比较
    if (!objectIs(nextDeps[i], prevDeps[i])) {
      return false;
    }
  }
  
  return true;
}

// Object.is 的 polyfill(处理 NaN 和 ±0 的特殊情况)
function objectIs(a, b) {
  return (
    (a === b && (a !== 0 || 1 / a === 1 / b)) || 
    (a !== a && b !== b) // NaN === NaN
  );
}

完整的渲染流程中的 useMemo

1. 首次渲染(Mount)

javascript 复制代码
function mountMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 1. 创建新的 Hook 对象并添加到链表
  const hook = mountWorkInProgressHook();
  
  // 2. 获取依赖项
  const nextDeps = deps === undefined ? null : deps;
  
  // 3. 执行计算函数
  const nextValue = create();
  
  // 4. 存储值和依赖项
  hook.memoizedState = [nextValue, nextDeps];
  
  return nextValue;
}

2. 更新渲染(Update)

javascript 复制代码
function updateMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 1. 获取对应的 Hook 对象
  const hook = updateWorkInProgressHook();
  
  // 2. 获取新的依赖项
  const nextDeps = deps === undefined ? null : deps;
  
  // 3. 获取上一次存储的状态
  const prevState = hook.memoizedState;
  
  // 4. 比较依赖项
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 依赖未变化,返回缓存的值
        return prevState[0];
      }
    }
  }
  
  // 5. 依赖变化,重新计算
  const nextValue = create();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

与 React 渲染周期的整合

useMemo 的执行与 React 的渲染周期密切相关:

  1. Render 阶段useMemo 在组件函数执行期间被调用
  2. 计算时机:计算函数在渲染期间执行(不是副作用阶段)
  3. 缓存策略:缓存的值仅在渲染期间有效
javascript 复制代码
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps,
  renderLanes,
) {
  // 准备渲染环境
  prepareToUseHooks(current, workInProgress, renderLanes);
  
  try {
    // 执行组件函数,useMemo 在此期间被调用
    let nextChildren = Component(nextProps, ref);
    
    // 处理子节点...
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
    
    return workInProgress.child;
  } finally {
    // 清理 Hook 环境
    resetHooksAfterRender();
  }
}

性能考虑与优化细节

1. 记忆化策略的权衡

useMemo 的实现需要在多个方面进行权衡:

javascript 复制代码
// 伪代码:useMemo 的成本效益分析
function shouldUseMemo(create, deps) {
  const calculationCost = estimateCalculationCost(create);
  const comparisonCost = deps.length * SINGLE_COMPARISON_COST;
  const memoryCost = MEMORY_PER_MEMOIZED_VALUE;
  
  // 只有当计算成本 > (比较成本 + 内存成本) 时,使用 useMemo 才有意义
  return calculationCost > (comparisonCost + memoryCost);
}

2. 依赖项数组的特殊处理

javascript 复制代码
// 处理各种依赖项情况
function normalizeDeps(deps) {
  if (deps === undefined || deps === null) {
    // 没有提供依赖项,每次渲染都重新计算
    return null;
  }
  
  if (!Array.isArray(deps)) {
    // 开发环境下警告
    console.error('useMemo expects an array of dependencies.');
    return null;
  }
  
  return deps;
}

3. 开发环境下的额外检查

React 在开发环境下提供了额外的警告和检查:

javascript 复制代码
function useMemo(create, deps) {
  if (__DEV__) {
    // 验证 Hook 调用规则
    validateHookCall();
    
    // 检查依赖项是否是数组
    if (deps !== undefined && deps !== null && !Array.isArray(deps)) {
      console.error(
        'useMemo requires an array of dependencies. Got: %s',
        typeof deps,
      );
    }
    
    // 记录 Hook 的使用以便调试
    recordHook();
  }
  
  // ... 实际实现
}

与 useCallback 的关系

useCallback 实际上是 useMemo 的特例:

javascript 复制代码
function useCallback(callback, deps) {
  return useMemo(() => callback, deps);
}

// 在 React 源码中的实际实现:
function updateCallback(callback, deps) {
  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];
      }
    }
  }
  
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

实际应用中的注意事项

1. 正确使用依赖项

javascript 复制代码
function MyComponent({ items, filter }) {
  // 正确:包含所有依赖项
  const filteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items, filter]); // ✓ 所有依赖项都包含

  // 错误:遗漏依赖项
  const badFilteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items]); // ✗ 遗漏了 filter 依赖
}

2. 避免过度优化

javascript 复制代码
function MyComponent({ value }) {
  // 不必要的 useMemo:计算很简单
  const simpleValue = useMemo(() => value + 1, [value]); // ✗ 过度优化
  
  // 必要的 useMemo:计算很昂贵
  const expensiveValue = useMemo(() => {
    return expensiveCalculation(value);
  }, [value]); // ✓ 合理的优化
}

总结

useMemo 的实现原理可以概括为:

  1. 基于 Hook 机制:利用 React 的 Hook 链表结构存储记忆的值和依赖项
  2. 依赖比较:使用严格比较(Object.is)检查依赖项是否变化
  3. 记忆化策略:依赖未变化时返回缓存值,变化时重新计算
  4. 渲染期间执行:计算函数在渲染阶段执行,不是副作用
  5. 性能权衡:在计算成本、比较成本和内存使用之间取得平衡
相关推荐
夕水4 小时前
原生js实现常规ui组件之checkbox篇
前端·javascript
编程二级爱好者4 小时前
2025年9月计算机二级Web程序设计——选择题打卡Day5
前端·计算机二级
Tanjc5184 小时前
uniapp H5预览图片组件
前端·vue.js·uni-app
ᥬ 小月亮4 小时前
uniapp中输入金额的过滤(只允许输入数字和小数点)
前端·css·uni-app
共享ui设计和前端开发4 小时前
UI前端大数据可视化实战策略:如何设计符合用户认知的数据可视化界面?
前端·ui·信息可视化
Akshsjsjenjd4 小时前
Ansible 变量与加密文件全解析:从基础定义到安全实践
前端·安全·ansible
2503_928411564 小时前
9.2 BOM对象
前端·javascript
whysqwhw4 小时前
JavaScript 动态代理全面指南
前端
Highcharts.js5 小时前
Highcharts Stock 股票图在交易系统中的应用思路
前端·数据可视化·股票图