React useMemo 深度源码解析
一、整体架构认知
┌─────────────────────────────────────────────────────┐
│ React 应用 │
│ │
│ useMemo(fn, deps) │
│ │ │
│ ▼ │
│ ReactCurrentDispatcher ← 根据阶段切换 dispatcher │
│ │ │
│ ┌────┴─────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ mount阶段 update阶段 │
│ mountMemo() updateMemo() │
│ │ │ │
│ ▼ ▼ │
│ 执行fn,存结果 比较deps │
│ 存到hook.memo │ │
│ State上 ├─ 相同 → 返回缓存值 │
│ └─ 不同 → 重新执行fn │
│ │
│ 底层存储: Fiber.memoizedState │
│ (hook 单向链表) │
└─────────────────────────────────────────────────────┘
二、从调用入口开始
1. useMemo 的入口定义
// packages/react/src/ReactHooks.js
/**
* useMemo 的对外API入口
* 它本身不包含任何逻辑,只是一个"转发器"
* 真正的实现取决于当前的 dispatcher
*/
function useMemo<T>(
create: () => T, // 工厂函数,返回需要缓存的值
deps: Array<mixed> | void | null // 依赖数组
): T {
// 获取当前的 dispatcher
// 这是 React Hooks 能工作的关键机制
const dispatcher = resolveDispatcher();
// 委托给 dispatcher 上的 useMemo
return dispatcher.useMemo(create, deps);
}
2. Dispatcher 切换机制
// packages/react-reconciler/src/ReactFiberHooks.js
/**
* React 内部维护了一个全局变量 ReactCurrentDispatcher
* 在组件渲染的不同阶段,会被赋予不同的 dispatcher 对象
*
* 这就是为什么 hooks 只能在组件函数体内调用:
* 只有在渲染过程中,dispatcher 才会被正确设置
*/
// ====== 不同阶段的 dispatcher 对象 ======
// 首次渲染(mount)时的 dispatcher
const HooksDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
useMemo: mountMemo, // ← mount 阶段
useCallback: mountCallback,
useRef: mountRef,
// ...其他hooks
};
// 更新(update)时的 dispatcher
const HooksDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
useMemo: updateMemo, // ← update 阶段
useCallback: updateCallback,
useRef: updateRef,
// ...其他hooks
};
// 非法调用时的 dispatcher(开发环境下会报错提示)
const ContextOnlyDispatcher = {
useMemo: throwInvalidHookError,
// ... 所有 hooks 都指向报错函数
};
/**
* 在 renderWithHooks 中切换 dispatcher
* 这是每次组件渲染的入口函数
*/
function renderWithHooks(
current: Fiber | null, // 当前屏幕上的 fiber(null 表示首次渲染)
workInProgress: Fiber, // 正在构建的新 fiber
Component: Function, // 组件函数
props: any,
secondArg: any,
nextRenderLanes: Lanes
): any {
// 保存当前正在渲染的 fiber
currentlyRenderingFiber = workInProgress;
// 重置 hook 链表(重要!每次渲染都从头开始遍历)
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
// ★★★ 关键:根据是否有 current fiber 来判断mount/update ★★★
if (current !== null && current.memoizedState !== null) {
// 更新阶段:fiber 已存在,有之前的 hook 状态
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
} else {
// 挂载阶段:首次渲染
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
}
// ★ 执行组件函数 — 此时组件内的 hooks 调用会走对应的 dispatcher
let children = Component(props, secondArg);
// 渲染完毕,切回非法 dispatcher(防止在组件外调用 hooks)
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// 重置全局变量
currentlyRenderingFiber = null;
currentHook = null;
workInProgressHook = null;
return children;
}
三、Fiber 上的 Hook 数据结构
/**
* ====== 核心数据结构 ======
*
* 每个 hook 调用对应一个 Hook 对象
* 所有 Hook 对象通过 next 指针串成单向链表
* 链表挂在 Fiber.memoizedState 上
*/
// Hook 对象的结构
type Hook = {
memoizedState: any, // 存储 hook 的状态/缓存值
baseState: any, // 基础状态(用于 useState/useReducer)
baseQueue: Update<any> | null,
queue: UpdateQueue<any> | null,
next: Hook | null, // 指向下一个 hook ← 形成链表
};
/**
* 直观理解 Hook 链表:
*
* function MyComponent() {
* const [count, setCount] = useState(0); // Hook1
* const doubled = useMemo(() => count*2, [count]); // Hook2
* const ref = useRef(null); // Hook3
* useEffect(() => { ... }, [count]); // Hook4
* return <div>{doubled}</div>;
* }
*
* Fiber.memoizedState 指向:
*
* Hook1 (useState) ──next──→ Hook2 (useMemo) ──next──→ Hook3 (useRef) ──next──→ Hook4 (useEffect) ──next──→ null
* │ │
* │ memoizedState: 0 │ memoizedState: [cachedValue, deps]
*
* ⚠️ 这就是为什么 hooks 不能放在条件语句中!
* 因为每次渲染必须保证 hook 链表的顺序一致
* React 靠"调用顺序"来匹配新旧 hook
*/
Fiber 节点
┌──────────────────────────────┐
│ type: MyComponent │
│ memoizedState ───────────┐ │
│ ... │ │
└──────────────────────────┼───┘
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Hook #1 │ │ Hook #2 │ │ Hook #3 │
│ useState │────→│ useMemo │────→│ useRef │──→ null
│ │ │ │ │ │
│ memoized: │ │ memoized: │ │ memoized: │
│ 0 │ │ [value,deps]│ │ {current:x} │
└─────────────┘ └─────────────┘ └─────────────┘
四、Mount 阶段源码 — mountMemo
// packages/react-reconciler/src/ReactFiberHooks.js
/**
* mountMemo - 首次渲染时 useMemo 的实现
*
* 职责:
* 1. 创建新的 hook 节点并加入链表
* 2. 执行工厂函数得到初始值
* 3. 将值和依赖数组一起缓存到 hook.memoizedState
*/
function mountMemo<T>(
nextCreate: () => T, // 工厂函数
deps: Array<mixed> | void | null // 依赖数组
): T {
// ★ 第1步:创建 hook 对象并挂到链表上
const hook = mountWorkInProgressHook();
// 处理 deps(undefined 和 null 有不同含义)
const nextDeps = deps === undefined ? null : deps;
// ★ 第2步:标记当前 Fiber 允许在 render 阶段有副作用
// 这是 React 19 新增的,用于 useMemo 的开发环境 double-invoke
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate(); // 开发环境下多调用一次,检测副作用
}
// ★ 第3步:执行工厂函数,得到需要缓存的值
const nextValue = nextCreate();
// ★ 第4步:将 [值, 依赖] 存到 hook.memoizedState
// 这是 useMemo 的核心存储格式:一个包含两个元素的数组
hook.memoizedState = [nextValue, nextDeps];
// 返回计算出的值
return nextValue;
}
/**
* mountWorkInProgressHook - 创建新 hook 并接入链表
*
* 这是所有 hooks 在 mount 阶段共用的创建函数
*/
function mountWorkInProgressHook(): Hook {
// 创建新的 hook 对象
const hook: Hook = {
memoizedState: null, // 将存储 [value, deps]
baseState: null,
baseQueue: null,
queue: null,
next: null, // 链表指针
};
if (workInProgressHook === null) {
// ★ 情况1:这是第一个 hook
// 将它设为 Fiber 的 memoizedState(链表头)
currentlyRenderingFiber.memoizedState = hook;
workInProgressHook = hook;
} else {
// ★ 情况2:已有前置 hook,接到链表尾部
workInProgressHook.next = hook;
workInProgressHook = hook;
}
return hook;
}
Mount 过程图解
调用 useMemo(() => expensiveCalc(a, b), [a, b])
│
▼
mountMemo(create, deps)
│
┌──────────────┼──────────────┐
▼ ▼ ▼
创建Hook 执行create() 存储结果
│ │ │
▼ ▼ ▼
hook = { nextValue = hook.memoizedState =
memoized- create() [nextValue, deps]
State:null // 执行计算 ↑
next: null 这个数组就是缓存
} [计算结果, [a, b]]
│
▼
接入Fiber链表
fiber.memoizedState → hook1 → hook2(useMemo) → ...
五、Update 阶段源码 — updateMemo
/**
* updateMemo - 更新渲染时 useMemo 的实现
*
* 核心逻辑:
* 1. 取出上一次缓存的 [value, deps]
* 2. 浅比较新旧 deps
* 3. 相同则返回缓存值,不同则重新计算
*/
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null
): T {
// ★ 第1步:获取当前 hook(从链表中按顺序取出)
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// ★ 第2步:取出上一次的缓存值
const prevState = hook.memoizedState;
// prevState = [previousValue, previousDeps]
if (nextDeps !== null) {
// ★ 第3步:取出上一次的依赖数组
const prevDeps: Array<mixed> | null = prevState[1];
// ★ 第4步:逐个浅比较依赖项(核心!)
if (areHookInputsEqual(nextDeps, prevDeps)) {
// ✅ 依赖没变,直接返回缓存的值
// 不执行 nextCreate(),这就是"跳过计算"的关键
return prevState[0];
}
}
// ❌ 依赖变了(或没有提供deps),需要重新计算
// 开发环境 double invoke 检测
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
// ★ 第5步:执行工厂函数得到新值
const nextValue = nextCreate();
// ★ 第6步:更新缓存
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
/**
* updateWorkInProgressHook - 从旧链表中复用/创建 hook
*
* 这是 update 阶段所有 hooks 共用的
* 关键作用:按顺序从旧 fiber 的 hook 链表中取出对应的 hook
*
* 这也是为什么 hook 顺序不能变的根本原因:
* 第N次调用 updateWorkInProgressHook 就取出链表第N个节点
*/
function updateWorkInProgressHook(): Hook {
// ★ nextCurrentHook 指向旧 fiber 链表中的下一个 hook
let nextCurrentHook: Hook | null;
if (currentHook === null) {
// 第一个 hook,从旧 fiber 的链表头开始
const current = currentlyRenderingFiber.alternate; // alternate 指向旧 fiber
if (current !== null) {
nextCurrentHook = current.memoizedState; // 旧 fiber 的 hook 链表头
} else {
nextCurrentHook = null;
}
} else {
// 后续 hook,沿链表向下走
nextCurrentHook = currentHook.next;
}
// 对应到新 fiber 链表
let nextWorkInProgressHook: Hook | null;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// 已经存在的 hook(re-render 场景)
workInProgressHook = nextWorkInProgressHook;
currentHook = nextCurrentHook;
} else {
// ★ 从旧 hook 克隆一个新 hook
if (nextCurrentHook === null) {
// 不应该出现的情况,说明 hook 数量发生了变化
// 这就是条件调用 hooks 会出错的根本原因!
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
throw new Error('...');
} else {
throw new Error(
'Rendered more hooks than during the previous render.'
);
}
}
currentHook = nextCurrentHook;
// 创建新 hook,复制旧 hook 的数据
const newHook: Hook = {
memoizedState: currentHook.memoizedState, // ← 复制缓存
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
// 接入新链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = newHook;
workInProgressHook = newHook;
} else {
workInProgressHook.next = newHook;
workInProgressHook = newHook;
}
}
return workInProgressHook;
}
Update 过程图解
重新渲染时调用 useMemo(() => expensiveCalc(a, b), [a, b])
│
▼
updateMemo(create, [a, b])
│
┌──────────┴──────────┐
▼ ▼
从链表取出对应Hook 比较依赖数组
(updateWorkInProgress- areHookInputsEqual(
Hook) [a, b], ← 新deps
│ [a_old, b_old])← 旧deps
▼ │
hook.memoizedState = ┌───┴───┐
[oldValue, [a_old,b_old]] │ │
相同 不同
│ │
▼ ▼
返回旧值 执行create()
oldValue newValue
│ │
│ ▼
│ hook.memoizedState =
│ [newValue, [a, b]]
│ │
▼ ▼
返回给组件使用
六、依赖比较算法 — areHookInputsEqual
/**
* areHookInputsEqual - 所有 hooks 共用的依赖比较函数
*
* 这是 useMemo、useCallback、useEffect 判断是否需要更新的核心
* 使用的是 Object.is 浅比较
*/
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null
): boolean {
// ★ 情况1:上一次没有 deps(不应该出现,但做兼容)
if (prevDeps === null) {
// 开发环境下会有 warning
if (__DEV__) {
console.error(
'%s received a final argument during this render, ' +
'but not during the previous render. ' +
'Even though the final argument is optional, ' +
'its type cannot change between renders.',
currentHookNameInDev
);
}
return false; // 视为不相等,重新计算
}
// ★ 情况2:开发环境检查 deps 长度变化
if (__DEV__) {
if (nextDeps.length !== prevDeps.length) {
console.error(
'The final argument passed to %s changed size between renders. ' +
'The order and size of this array must remain constant.\n\n' +
'Previous: [%s]\n' +
'Incoming: [%s]',
currentHookNameInDev,
prevDeps.join(', '),
nextDeps.join(', ')
);
}
}
// ★ 情况3:逐项使用 Object.is 比较
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 的polyfill
* 与 === 的区别:
* - Object.is(NaN, NaN) → true (=== 返回 false)
* - Object.is(+0, -0) → false (=== 返回 true)
*/
function is(x: any, y: any): boolean {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) ||
(x !== x && y !== y) // NaN check
);
}
比较过程图解
areHookInputsEqual([newA, newB, newC], [oldA, oldB, oldC])
i=0: Object.is(newA, oldA) → true ✓ continue
i=1: Object.is(newB, oldB) → true ✓ continue
i=2: Object.is(newC, oldC) → false ✗ return false
结果:不相等 → 重新执行 create 函数
Object.is 比较规则:
┌──────────────────────────────────────────┐
│ Object.is(1, 1) → true │
│ Object.is('a', 'a') → true │
│ Object.is(null, null) → true │
│ Object.is(obj, obj) → true (同一个引用) │
│ Object.is(obj, {...obj}) → false (不同引用!) │
│ Object.is([1,2], [1,2]) → false (不同引用!) │
│ Object.is(NaN, NaN) → true (特殊)│
│ Object.is(+0, -0) → false (特殊)│
└──────────────────────────────────────────┘
⚠️ 这意味着:
每次渲染创建新对象/数组作为dep → 永远不相等 → useMemo 永远失效!
七、React Compiler(React 19 Forget)中的 useMemo
/**
* React Compiler 会自动分析代码并插入缓存逻辑
* 理解编译器做了什么有助于理解 useMemo 的本质
*/
// ====== 你写的代码 ======
function ProductPage({ productId, referrer }) {
const product = useProduct(productId);
// 你需要手动写 useMemo 来避免重新计算
const visibleProducts = useMemo(
() => filterProducts(products, category),
[products, category]
);
return <ProductList products={visibleProducts} />;
}
// ====== React Compiler 编译后(概念性)======
function ProductPage({ productId, referrer }) {
const product = useProduct(productId);
// 编译器自动生成的缓存逻辑(使用内部 hook)
// 类似 useMemoCache —— 编译器专用的缓存 hook
const $ = useMemoCache(4); // 申请4个缓存槽位
let visibleProducts;
// 槽位0: 缓存 products
// 槽位1: 缓存 category
// 槽位2: 缓存 filterProducts 的结果
if ($[0] !== products || $[1] !== category) {
visibleProducts = filterProducts(products, category);
$[0] = products;
$[1] = category;
$[2] = visibleProducts;
} else {
visibleProducts = $[2];
}
// 槽位3: 缓存 JSX 元素
let t0;
if ($[3] !== visibleProducts) {
t0 = <ProductList products={visibleProducts} />;
$[3] = visibleProducts;
} else {
t0 = $[3]; // 复用缓存的 JSX
}
return t0;
}
/**
* useMemoCache — React 编译器使用的内部 hook
*
* 与 useMemo 的区别:
* 1. useMemo: 一个 hook 缓存一个值
* 2. useMemoCache: 一个 hook 提供多个缓存槽位
* 编译器将整个组件的所有缓存需求合并到一个缓存数组中
*/
function useMemoCache(size: number): Array<any> {
const hook = updateWorkInProgressHook();
let memoCache = hook.memoizedState;
if (memoCache == null) {
// 初始化缓存数组
memoCache = {
data: new Array(size).fill(MEMO_CACHE_SENTINEL),
index: 0,
};
hook.memoizedState = memoCache;
}
return memoCache.data;
}
八、useMemo vs useCallback 源码对比
/**
* useMemo 和 useCallback 的实现几乎完全一样
* 唯一的区别是:缓存的是"返回值"还是"函数本身"
*/
// ====== useMemo: 缓存值 ======
function mountMemo<T>(nextCreate: () => T, deps): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate(); // ★ 执行函数,拿到返回值
hook.memoizedState = [nextValue, nextDeps]; // ★ 缓存 返回值
return nextValue; // ★ 返回 值
}
// ====== useCallback: 缓存函数 ======
function mountCallback<T>(callback: T, deps): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// ★ 注意区别:没有执行 callback!直接缓存函数本身
hook.memoizedState = [callback, nextDeps]; // ★ 缓存 函数
return callback; // ★ 返回 函数
}
/**
* 所以:
* useCallback(fn, deps) ≡ useMemo(() => fn, deps)
*
* useCallback 只是 useMemo 的语法糖!
*/
// ====== update 阶段对比 ======
function updateMemo<T>(nextCreate: () => T, deps): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (nextDeps !== null) {
if (areHookInputsEqual(nextDeps, prevState[1])) {
return prevState[0]; // 返回缓存的 值
}
}
const nextValue = nextCreate(); // ★ 重新执行
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateCallback<T>(callback: T, deps): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (nextDeps !== null) {
if (areHookInputsEqual(nextDeps, prevState[1])) {
return prevState[0]; // 返回缓存的 函数
}
}
// ★ 不执行,直接存新函数
hook.memoizedState = [callback, nextDeps];
return callback;
}
九、实际案例与常见陷阱
陷阱1:依赖项是新对象引用
function BadExample({ items }) {
// ❌ 每次渲染 filter 都返回新数组引用
// Object.is([], []) === false
// 所以 useMemo 每次都重新计算,完全失效!
const filtered = useMemo(
() => items.filter(i => i.active),
[items.filter(i => i.active)] // ← 新数组!每次都不相等
);
// ✅ 正确做法:依赖原始数据
const filtered = useMemo(
() => items.filter(i => i.active),
[items] // ← items 引用没变就不重算
);
}
陷阱2:deps 为空/undefined/null 的区别
function DepsDemo() {
// 情况1: 有依赖数组 → 正常的缓存行为
const a = useMemo(() => compute(), [x, y]);
// 情况2: 空依赖数组 → 只计算一次,永远返回缓存值(类似常量)
const b = useMemo(() => compute(), []);
// 情况3: 没有第二个参数(undefined)→ 每次渲染都重新计算!
// useMemo 完全失去意义
const c = useMemo(() => compute());
// 情况4: 显式传 null → 和 undefined 一样,每次都重新计算
const d = useMemo(() => compute(), null);
}
从源码看为什么
/**
* 追踪 deps = undefined 的执行路径
*/
// ====== mount 阶段 ======
function mountMemo(nextCreate, deps) {
const hook = mountWorkInProgressHook();
// undefined → null
const nextDeps = deps === undefined ? null : deps;
// nextDeps = null
const nextValue = nextCreate();
// 存储:[value, null]
hook.memoizedState = [nextValue, null];
// ↑ deps 是 null
return nextValue;
}
// ====== update 阶段 ======
function updateMemo(nextCreate, deps) {
const hook = updateWorkInProgressHook();
// 又是 undefined → null
const nextDeps = deps === undefined ? null : deps;
// nextDeps = null
const prevState = hook.memoizedState;
// prevState = [oldValue, null]
// ★ 关键!nextDeps 是 null,跳过整个 if 块!
if (nextDeps !== null) { // null !== null → false
const prevDeps = prevState[1]; // 永远不会执行到这里
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]; // 永远不会执行到这里
}
}
// ★ 直接到这里:每次都重新执行!
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
/**
* 而 deps = [] 的执行路径:
*/
function updateMemo_emptyDeps(nextCreate, deps) {
const hook = updateWorkInProgressHook();
const nextDeps = []; // 空数组,但不是 null
const prevState = hook.memoizedState;
// prevState = [oldValue, []]
// ★ [] !== null → true,进入 if
if (nextDeps !== null) {
const prevDeps = prevState[1]; // []
// areHookInputsEqual([], [])
// for循环遍历 0 个元素 → 直接返回 true
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]; // ★ 永远返回缓存值!
}
}
// 永远不会执行到这里
}
三种情况的完整流程图
useMemo(create, deps)
│
▼
nextDeps = (deps === undefined) ? null : deps
│
├── deps = [a, b] deps = [] deps = undefined/null
│ │ │ │
│ ▼ ▼ ▼
│ nextDeps = [a,b] nextDeps = [] nextDeps = null
│ │ │ │
│ ▼ ▼ ▼
│ if(nextDeps!==null) if(nextDeps!==null) if(nextDeps!==null)
│ → TRUE ✓ → TRUE ✓ → FALSE ✗
│ │ │ │
│ ▼ ▼ │
│ 比较每一项dep for循环0次 │
│ Object.is逐一对比 直接return true │
│ │ │ │
│ ┌──┴──┐ ▼ │
│ ▼ ▼ 返回缓存值 │
│ 相同 不同 (永不重算) │
│ │ │ │
│ ▼ ▼ ▼
│ 返回 重新计算 每次都重新计算
│ 缓存值 create() create()
│
└── 这是正常使用场景
十、陷阱3:在 useMemo 内部产生副作用
// ❌ 错误:useMemo 中不应有副作用
function BadSideEffect() {
const data = useMemo(() => {
// 这些都不应该出现在 useMemo 中
document.title = 'Updated'; // DOM操作
localStorage.setItem('key', 'v'); // 存储操作
console.log('computed!'); // 在Strict Mode下会double invoke
fetchData(); // 网络请求
return expensiveCalc();
}, [dep]);
}
源码层面的原因
/**
* React 18 的 Strict Mode 和 React 19 的编译器
* 都会在开发环境 double invoke create 函数
*/
function mountMemo<T>(nextCreate: () => T, deps): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// ★ 开发环境下,额外调用一次 create 函数!
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate(); // 第1次调用(结果被丢弃)
}
const nextValue = nextCreate(); // 第2次调用(使用这个结果)
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
/**
* 为什么要 double invoke?
*
* React 未来的 Concurrent 特性(如 Offscreen/Activity)
* 可能会:
* 1. 暂停渲染,稍后恢复
* 2. 丢弃某次渲染结果重新来
* 3. 同一次更新中多次调用 render
*
* 如果 useMemo 中有副作用,这些场景下会出bug
* double invoke 就是帮你提前发现这类问题
*
* ┌──────────────────────────────────────┐
* │ Concurrent Mode 下的渲染可能: │
* │ │
* │ render开始 → 暂停 → 丢弃 → 重新render │
* │ ↑ │
* │ 这里如果有副作用就会执行多次! │
* └──────────────────────────────────────┘
*/
十一、陷阱4:过度使用 useMemo
// ❌ 没必要的 useMemo — 简单计算反而更慢
function OverMemoized({ firstName, lastName }) {
// 字符串拼接非常快,useMemo 的开销比计算本身还大
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);
// ✅ 直接计算
const fullName = `${firstName} ${lastName}`;
}
从源码分析 useMemo 的额外开销
/**
* useMemo 每次渲染(即使命中缓存)都需要执行的操作:
*/
function updateMemo(nextCreate, deps) {
// 开销1:从链表中取出 hook 对象
// - 涉及多个指针操作和条件判断
const hook = updateWorkInProgressHook(); // ~10 行逻辑
// 开销2:处理 deps
const nextDeps = deps === undefined ? null : deps;
// 开销3:取旧状态
const prevState = hook.memoizedState;
if (nextDeps !== null) {
const prevDeps = prevState[1];
// 开销4:逐个比较依赖项
// - 创建函数调用栈
// - 循环 + Object.is
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
// 如果没命中,还有:
// 开销5:执行 create 函数
// 开销6:创建新数组 [value, deps]
// 开销7:写入 hook.memoizedState
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
/**
* ★ 总结:useMemo 的"底价"
*
* 即使缓存命中(最理想情况),每次渲染仍需:
* 1. updateWorkInProgressHook() — 链表遍历、指针操作
* 2. 取出 prevState
* 3. areHookInputsEqual() — 函数调用 + 循环比较
*
* 如果你缓存的计算比这些操作还简单(如字符串拼接、简单加减)
* 那 useMemo 反而是负优化
*
* ┌─────────────────────────────────────────┐
* │ 适合 useMemo 的场景: │
* │ ✅ 大数组的 filter/map/sort/reduce │
* │ ✅ 递归计算 / 深层遍历 │
* │ ✅ 创建复杂对象(作为子组件的 props) │
* │ ✅ 正则匹配大量文本 │
* │ │
* │ 不适合 useMemo 的场景: │
* │ ❌ 简单的算术运算 │
* │ ❌ 字符串拼接 │
* │ ❌ 基本类型的直接返回 │
* │ ❌ 每次 deps 都会变的计算 │
* └─────────────────────────────────────────┘
*/
十二、陷阱5:引用稳定性问题
function ReferenceStability({ items, threshold }) {
// ❌ 即使 items 没变,每次渲染都传入新的内联函数作为 filter
// 但这里不影响 useMemo,因为 filter函数不在 deps 中
const filtered = useMemo(() => {
return items.filter(item => item.value > threshold);
}, [items, threshold]); // deps 中的是 items 和 threshold
// ★ 关键问题出在这里:
// filtered 引用是否稳定?取决于 items/threshold 是否变化
// 如果 filtered 作为子组件的 prop:
return <ChildComponent data={filtered} />;
// 当 items/threshold 没变时:
// filtered 引用不变 → ChildComponent 配合 React.memo 可以跳过渲染
// 当 items/threshold 变了时:
// filtered 是新数组 → ChildComponent 必须重新渲染
// ❌ 对比没有 useMemo 的情况:
const filteredBad = items.filter(item => item.value > threshold);
// 每次渲染都是新引用 → ChildComponent 的 React.memo 永远失效
}
十三、Re-render 阶段的特殊处理
/**
* React 还有一个 re-render dispatcher
* 当在 render 过程中触发了 setState(比如在 render 中调用 dispatch)
* React 会立即重新渲染该组件
*
* 在这个场景下,useMemo 走的是 rerenderMemo
*/
const HooksDispatcherOnRerender = {
useMemo: updateMemo, // 注意:re-render 时复用 updateMemo
useState: rerenderState,
useReducer: rerenderReducer,
// ...
};
/**
* 完整的 dispatcher 切换流程:
*
* renderWithHooks()
* │
* ┌───────────┴────────────┐
* │ │
* current === null? current !== null?
* │ │
* ▼ ▼
* HooksDispatcherOnMount HooksDispatcherOnUpdate
* │ │
* └───────────┬────────────┘
* │
* Component(props) ← 执行组件函数
* │
* render中触发了setState?
* │
* ┌───────┴───────┐
* │ │
* 否 是
* │ │
* ▼ ▼
* 正常结束 切换到 HooksDispatcherOnRerender
* │
* 再次执行 Component(props)
* │
* 使用 updateMemo/rerenderState...
*/
十四、React 19 中 useMemo 的变化
/**
* React 19 带来的变化:
*
* 1. React Compiler(原 React Forget)
* - 自动插入 memoization,不再需要手写 useMemo
* - 使用 useMemoCache 代替多个独立的 useMemo
*
* 2. 新的 cache() API(用于服务端组件)
* - 请求级别的缓存,与 useMemo 互补
*/
// ====== React 19 之前 ======
function ProductPage({ productId }) {
const product = useProduct(productId);
// 手动写 useMemo
const recommendations = useMemo(
() => computeRecommendations(product),
[product]
);
// 手动写 useCallback
const handleAdd = useCallback(
() => addToCart(product),
[product]
);
return (
<div>
{/* 手动写 React.memo */}
<MemoizedRecommendations items={recommendations} />
<MemoizedButton onClick={handleAdd} />
</div>
);
}
// ====== React 19 + Compiler 之后 ======
// 编译器自动处理,你只需要写纯净的代码
function ProductPage({ productId }) {
const product = useProduct(productId);
// 直接写,编译器自动判断是否需要缓存
const recommendations = computeRecommendations(product);
const handleAdd = () => addToCart(product);
return (
<div>
{/* 编译器自动处理组件级别的 memo */}
<Recommendations items={recommendations} />
<Button onClick={handleAdd} />
</div>
);
}
/**
* 编译器编译后的内部实现(简化版):
*/
function ProductPage_compiled({ productId }) {
const $ = useMemoCache(6); // 6个缓存槽位
const product = useProduct(productId);
let recommendations;
if ($[0] !== product) {
recommendations = computeRecommendations(product);
$[0] = product;
$[1] = recommendations;
} else {
recommendations = $[1];
}
let handleAdd;
if ($[2] !== product) {
handleAdd = () => addToCart(product);
$[2] = product;
$[3] = handleAdd;
} else {
handleAdd = $[3];
}
let jsx;
if ($[4] !== recommendations || $[5] !== handleAdd) {
jsx = (
<div>
<Recommendations items={recommendations} />
<Button onClick={handleAdd} />
</div>
);
$[4] = recommendations;
$[5] = handleAdd;
} else {
jsx = $[5]; // 连 JSX 都被缓存了!
}
return jsx;
}
十五、完整生命周期总结
┌─────────────────────────────────────────────────────────────┐
│ useMemo 完整生命周期 │
│ │
│ ┌─────── 首次渲染 (Mount) ───────┐ │
│ │ │ │
│ │ 1. renderWithHooks() │ │
│ │ dispatcher = OnMount │ │
│ │ │ │
│ │ 2. Component() 执行 │ │
│ │ ↓ │ │
│ │ 3. useMemo(create, deps) │ │
│ │ ↓ │ │
│ │ 4. mountMemo() │ │
│ │ - mountWorkInProgressHook()│ ← 创建Hook节点 │
│ │ - create() 执行 │ ← 计算值 │
│ │ - hook.memoizedState = │ │
│ │ [value, deps] │ ← 存储缓存 │
│ │ - return value │ │
│ │ │ │
│ └────────────────────────────────┘ │
│ │ │
│ ▼ (用户交互/状态变更触发重渲染) │
│ │
│ ┌─────── 更新渲染 (Update) ──────┐ │
│ │ │ │
│ │ 1. renderWithHooks() │ │
│ │ dispatcher = OnUpdate │ │
│ │ │ │
│ │ 2. Component() 执行 │ │
│ │ ↓ │ │
│ │ 3. useMemo(create, newDeps) │ │
│ │ ↓ │ │
│ │ 4. updateMemo() │ │
│ │ - updateWorkInProgress- │ │
│ │ Hook() │ ← 从链表取旧Hook │
│ │ - prevState = │ │
│ │ hook.memoizedState │ ← 读取 [oldVal, oldDeps]│
│ │ ↓ │ │
│ │ 5. areHookInputsEqual( │ │
│ │ newDeps, oldDeps) │ ← 逐项 Object.is 比较 │
│ │ ↓ │ │
│ │ ┌──┴──────────┐ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ 相等 不相等 │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ return create()执行 │ │
│ │ oldValue newValue │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ hook.memoized- │ │
│ │ State = │ │
│ │ [newVal, newDeps] │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ return newValue │ │
│ │ │ │
│ └────────────────────────────────┘ │
│ │ │
│ ▼ (组件卸载) │
│ │
│ ┌─────── 卸载 (Unmount) ─────────┐ │
│ │ │ │
│ │ Fiber 节点被删除 │ │
│ │ hook 链表随之释放 │ │
│ │ 缓存的值被 GC 回收 │ │
│ │ │ │
│ │ ⚠️ useMemo 没有 cleanup │ │
│ │ (与 useEffect 不同) │ │
│ │ │ │
│ └────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
十六、设计哲学与核心总结
┌─────────────────────────────────────────────────────────┐
│ useMemo 设计哲学 │
│ │
│ 1. 最小存储原则 │
│ 只存 [value, deps],不存 create 函数 │
│ 因为 create 每次渲染都是新的闭包 │
│ │
│ 2. 顺序调用契约 │
│ 依靠链表位置匹配新旧 hook │
│ 不需要 key/name,最小化运行时开销 │
│ 代价:不能条件调用 │
│ │
│ 3. 浅比较策略 │
│ Object.is 逐项比较,不做深比较 │
│ 深比较成本不可控,违背"可预测性能"的原则 │
│ │
│ 4. 无保证缓存 │
│ 官方文档明确说:React 可能在某些情况下 │
│ 丢弃缓存重新计算(如 offscreen 场景) │
│ useMemo 是性能优化手段,不是语义保证 │
│ │
│ 5. 与 Concurrent Mode 的协作 │
│ render 阶段是纯的、可中断的、可重复的 │
│ useMemo 的 create 函数必须是纯函数 │
│ 这样即使被多次调用也不会出问题 │
│ │
│ 核心一句话: │
│ useMemo 用一个 [value, deps] 数组 + Object.is 浅比较 │
│ 实现了"跳过不必要的重复计算"这一优化 │
└─────────────────────────────────────────────────────────┘