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
};
对于 useMemo
,memoizedState
字段存储的是一个数组 [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 的渲染周期密切相关:
- Render 阶段 :
useMemo
在组件函数执行期间被调用 - 计算时机:计算函数在渲染期间执行(不是副作用阶段)
- 缓存策略:缓存的值仅在渲染期间有效
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
的实现原理可以概括为:
- 基于 Hook 机制:利用 React 的 Hook 链表结构存储记忆的值和依赖项
- 依赖比较:使用严格比较(Object.is)检查依赖项是否变化
- 记忆化策略:依赖未变化时返回缓存值,变化时重新计算
- 渲染期间执行:计算函数在渲染阶段执行,不是副作用
- 性能权衡:在计算成本、比较成本和内存使用之间取得平衡