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;
}
关键点解析
- mountWorkInProgressHook():在 fiber 节点上创建一个新的 Hook 对象,并将其添加到 Hook 链表中
- hook.memoizedState :存储了一个数组
[value, deps]value:缓存的计算结果deps:依赖数组
- 首次渲染必定执行 :
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;
}
关键点解析
- updateWorkInProgressHook():从当前 fiber 的 Hook 链表中获取对应的 Hook 对象
- areHookInputsEqual() :逐个比较依赖项,使用
Object.is()进行浅比较 - 短路优化 :如果依赖未变化,直接返回
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返回trueObject.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(如useState、useEffect等)
与 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 源码:
- Clone React 仓库:
bash
git clone https://github.com/facebook/react.git
cd react
- 构建 dev 版本:
bash
yarn
yarn build react/index,react-dom/index --type=NODE
- 在你的项目中链接本地 React:
bash
cd your-project
npm link ../react/build/node_modules/react
npm link ../react/build/node_modules/react-dom
- 在
ReactFiberHooks.js中添加console.log或debugger语句
总结
useMemo 的核心原理:
- 数据结构 :使用
[value, deps]存储缓存值和依赖 - 比较策略 :使用
Object.is浅比较依赖数组 - 两阶段实现 :
- Mount:直接执行并缓存
- Update:先比较依赖,命中则返回缓存,否则重新计算
- Hook 链表 :通过
next指针维护多个 Hook 的调用顺序
理解 useMemo 的源码实现,可以帮助我们:
- 正确使用依赖数组
- 避免过度优化
- 理解 React Hooks 的底层机制
- 更好地进行性能调优
参考资料
最后更新:2026-03-17