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. 性能权衡:在计算成本、比较成本和内存使用之间取得平衡
相关推荐
訾博ZiBo13 小时前
【文本朗读小工具】- 快速、免费的智能语音合成工具
前端
天蓝色的鱼鱼13 小时前
低代码是“未来”还是“骗局”?前端开发者有话说
前端
答案answer13 小时前
three.js着色器(Shader)实现数字孪生项目中常见的特效
前端·three.js
城管不管13 小时前
SpringBoot与反射
java·开发语言·前端
JackJiang13 小时前
即时通讯安全篇(三):一文读懂常用加解密算法与网络通讯安全
前端
一直_在路上13 小时前
Go架构师实战:玩转缓存,击破医疗IT百万QPS与“三大天灾
前端·面试
早八睡不醒午觉睡不够的程序猿14 小时前
Vue DevTools 调试提示
前端·javascript·vue.js
恋猫de小郭14 小时前
基于 Dart 的 Terminal UI ,pixel_prompt 这个 TUI 库了解下
android·前端·flutter
天天向上102414 小时前
vue el-form 自定义校验, 校验用户名调接口查重
前端·javascript·vue.js
忧郁的蛋~14 小时前
前端实现网页水印防移除的实战方案
前端