直接展示 React 源码(如 react-reconciler
包中的具体实现)会非常复杂,因为它涉及到 Fiber 架构、调度器(Scheduler)、优先级(Lanes)、并发模式(Concurrent Mode)等众多交织的概念。为了清晰地理解 useMemo
和 useCallback
的核心逻辑,我们将采用一种 高度简化但原理一致 的模拟实现方式来讲解。这种方式能帮助我们聚焦于这两个 Hook 本身的工作机制:依赖项比较 和 值的缓存/复用。
我们将模拟 React 内部处理 Hook 的基本流程,包括:
- Hook 状态存储 :每个 Hook 需要在组件的 Fiber 节点上存储其状态(比如
useMemo
存储的上一次计算结果和依赖项)。 - Dispatcher 机制:React 通过一个全局的 Dispatcher 对象来根据组件是首次渲染(mount)还是更新(update)来调用不同版本的 Hook 实现。
- 依赖项比较:核心逻辑,比较本次渲染传入的依赖项数组和上次存储的是否"相等"。
- 值/函数的创建与复用:根据依赖项比较结果决定是重新计算/创建,还是复用上一次的结果。
重要提示: 以下代码是 原理性模拟,并非 React 真实源码的拷贝。真实源码要复杂得多,但核心思想是一致的。我们的目标是通过这个模拟来理解其工作原理。
javascript
// ============================================================
// 模拟 React 内部环境和基本结构
// ============================================================
// 全局变量,模拟当前正在渲染的 Fiber 节点和当前处理到的 Hook 索引
// 在真实的 React 中,这些信息是通过闭包和内部模块传递的
let currentlyRenderingFiber = null; // 当前正在处理的 Fiber 节点 (模拟)
let workInProgressHook = null; // 当前正在处理的 Hook 节点 (模拟链表)
let hookIndex = 0; // 当前 Hook 在链表中的索引
// 模拟 Fiber 节点结构 (极度简化)
// 真实 Fiber 包含大量状态、优先级、子/父/兄弟指针等信息
class FiberNode {
constructor(tag, type) {
this.tag = tag; // 'FunctionComponent', 'HostComponent', etc.
this.type = type; // 对于函数组件,就是函数本身
this.memoizedState = null; // 存储该 Fiber 对应的 Hooks 链表 或 Class Component 的 state
// ... 其他很多属性
}
}
// 模拟 Hook 节点结构 (极度简化)
// 真实的 Hook 节点是一个链表,存储 memoizedState, baseState, queue, next 等
class HookNode {
constructor() {
this.memoizedState = null; // 存储 Hook 的状态 (例如 useState 的 state, useMemo 的 [value, deps])
this.next = null; // 指向下一个 Hook 节点
}
}
// 模拟 React 的调度和渲染入口 (极度简化)
function renderRoot(rootFiber) {
console.log(`[Render Start] Fiber: ${rootFiber.type.name}`);
currentlyRenderingFiber = rootFiber;
hookIndex = 0; // 重置 Hook 索引
workInProgressHook = currentlyRenderingFiber.memoizedState; // 获取上次渲染的 Hook 链表头
// 调用函数组件,执行其内部的 Hooks
const children = rootFiber.type({}); // 假设 props 是空对象
currentlyRenderingFiber = null; // 渲染结束,重置
workInProgressHook = null;
hookIndex = 0;
console.log(`[Render End] Fiber: ${rootFiber.type.name}`);
return children;
}
// ============================================================
// 核心:模拟 Dispatcher 机制
// ============================================================
// React 内部根据组件是 Mount 还是 Update,会切换不同的 Dispatcher
// Dispatcher 包含所有 Hook 的具体实现
// Mount 阶段的 Hook 实现
const HooksDispatcherOnMount = {
useMemo: mountMemo,
useCallback: mountCallback,
useState: mountState, // 假设还有 useState 等
// ...其他 Hooks
};
// Update 阶段的 Hook 实现
const HooksDispatcherOnUpdate = {
useMemo: updateMemo,
useCallback: updateCallback,
useState: updateState, // 假设还有 useState 等
// ...其他 Hooks
};
// 全局变量,模拟 React 当前使用的 Dispatcher
// React.currentDispatcher.current 在渲染函数组件前会被设置
let ReactCurrentDispatcher = {
current: null, // 指向 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate
};
// 辅助函数:获取当前正在处理的 Hook 节点
// 这个函数模拟了 React 如何在 Hook 调用时定位到对应的 Hook 存储单元
function resolveCurrentlyProcessingHook() {
if (!currentlyRenderingFiber) {
throw new Error("Hooks can only be called inside the body of a function component.");
}
// 尝试获取当前的 Hook 节点
const currentHook = workInProgressHook;
if (currentHook) {
// 如果不是第一个 Hook,移动到下一个 Hook 节点
workInProgressHook = currentHook.next;
} else {
// 如果是第一个 Hook 或者之前的 Hook 链表用完了 (Update 阶段发现新增 Hook 会报错)
// 在 Mount 阶段,我们会创建新的 Hook 节点并添加到链表尾部
if (currentlyRenderingFiber.memoizedState === null) {
// Fiber 的第一个 Hook
const newHook = new HookNode();
currentlyRenderingFiber.memoizedState = newHook;
workInProgressHook = newHook;
} else {
// Fiber 已经有 Hook 链表,找到链表尾部并添加新节点
// (简化:真实 React 在 Update 时如果 Hook 数量变化会报错)
// 这里我们简化处理,认为 Mount 阶段总能追加
let lastHook = currentlyRenderingFiber.memoizedState;
while (lastHook.next) {
lastHook = lastHook.next;
}
const newHook = new HookNode();
lastHook.next = newHook;
workInProgressHook = newHook;
}
// 在 Update 阶段,如果 workInProgressHook 为 null,意味着 Hook 数量或顺序发生变化,
// 真实 React 会抛出错误。这里简化,假设 Mount 阶段。
}
hookIndex++; // 移动索引
return workInProgressHook; // 返回当前处理的 Hook 节点
}
// ============================================================
// 核心:依赖项比较函数
// ============================================================
/**
* 比较新旧依赖项数组是否"浅相等"。
* React 使用 Object.is 来进行比较。
* @param {Array<any> | null | undefined} nextDeps 新的依赖项数组
* @param {Array<any> | null | undefined} prevDeps 旧的依赖项数组
* @returns {boolean} 如果依赖项相等则返回 true,否则返回 false
*/
function areDepsEqual(nextDeps, prevDeps) {
// console.log('[areDepsEqual] Comparing:', nextDeps, 'vs', prevDeps);
// 如果没有提供依赖项数组 (undefined),则每次都认为不相等,需要重新计算
if (prevDeps === null || prevDeps === undefined || nextDeps === null || nextDeps === undefined) {
// console.log('[areDepsEqual] Result: false (null/undefined deps)');
return false;
}
// 如果依赖项数组长度不同,则一定不相等
if (prevDeps.length !== nextDeps.length) {
// console.log('[areDepsEqual] Result: false (length mismatch)');
return false;
}
// 逐个比较依赖项,使用 Object.is
// Object.is 与 === 的主要区别在于:
// 1. Object.is(NaN, NaN) === true (=== 结果为 false)
// 2. Object.is(+0, -0) === false (=== 结果为 true)
// React 选择 Object.is 来处理这些边界情况
for (let i = 0; i < prevDeps.length; i++) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
// console.log(`[areDepsEqual] Result: false (mismatch at index ${i}: ${nextDeps[i]} vs ${prevDeps[i]})`);
return false;
}
}
// 所有依赖项都相等
// console.log('[areDepsEqual] Result: true');
return true;
}
// ============================================================
// `useMemo` 的模拟实现
// ============================================================
/**
* Mount 阶段的 useMemo 实现。
* @param {() => any} create 函数,用于计算 memoized 值
* @param {Array<any> | null | undefined} deps 依赖项数组
*/
function mountMemo(create, deps) {
// console.log(`[mountMemo] Hook Index: ${hookIndex}`);
const hook = resolveCurrentlyProcessingHook(); // 获取当前 Hook 存储节点
const nextDeps = deps === undefined ? null : deps; // 标准化 deps (undefined -> null)
console.log(`[mountMemo] Creating value for the first time. Deps:`, nextDeps);
const nextValue = create(); // 首次渲染,直接调用 create 函数计算值
// 将计算出的值和依赖项存储在 Hook 节点上
// 真实 React 中 memoizedState 结构更复杂,这里简化为 [value, deps]
hook.memoizedState = [nextValue, nextDeps];
// console.log(`[mountMemo] Stored state:`, hook.memoizedState);
return nextValue; // 返回计算出的值
}
/**
* Update 阶段的 useMemo 实现。
* @param {() => any} create 函数,用于计算 memoized 值
* @param {Array<any> | null | undefined} deps 依赖项数组
*/
function updateMemo(create, deps) {
const hook = resolveCurrentlyProcessingHook(); // 获取当前 Hook 存储节点
// console.log(`[updateMemo] Hook Index: ${hookIndex}. Previous state:`, hook.memoizedState);
if (!hook || !hook.memoizedState) {
// 这通常不应该发生,意味着 mount 时 state 没存好,或者 hook 链出错了
console.error("[updateMemo] Error: Hook state not found during update. Re-calculating.");
// 作为降级处理,重新计算(但不符合 React 行为)
// 真实 React 在 Hook 顺序或数量不对时会报错
return mountMemo(create, deps); // 借用 mount 逻辑(非标准行为)
}
const nextDeps = deps === undefined ? null : deps; // 标准化新的依赖项
const [prevState, prevDeps] = hook.memoizedState; // 获取上次存储的值和依赖项
// 核心逻辑:比较新旧依赖项
if (areDepsEqual(nextDeps, prevDeps)) {
// 如果依赖项没有变化,直接返回上次存储的值
console.log(`[updateMemo] Dependencies haven't changed. Reusing previous value:`, prevState);
return prevState;
} else {
// 如果依赖项变化了,重新调用 create 函数计算新值
console.log(`[updateMemo] Dependencies changed. Recalculating value. Prev Deps:`, prevDeps, `Next Deps:`, nextDeps);
const nextValue = create();
// 更新 Hook 节点上存储的值和依赖项
hook.memoizedState = [nextValue, nextDeps];
console.log(`[updateMemo] Stored new state:`, hook.memoizedState);
return nextValue; // 返回新计算的值
}
}
// ============================================================
// `useCallback` 的模拟实现
// ============================================================
// useCallback(fn, deps) 本质上等价于 useMemo(() => fn, deps)
/**
* Mount 阶段的 useCallback 实现。
* @param {Function} callback 需要被 memoize 的回调函数
* @param {Array<any> | null | undefined} deps 依赖项数组
*/
function mountCallback(callback, deps) {
// console.log(`[mountCallback] Hook Index: ${hookIndex}`);
const hook = resolveCurrentlyProcessingHook(); // 获取当前 Hook 存储节点
const nextDeps = deps === undefined ? null : deps; // 标准化 deps
console.log(`[mountCallback] Creating callback for the first time. Deps:`, nextDeps);
const nextCallback = callback; // 首次渲染,直接使用传入的 callback
// 将 callback 本身和依赖项存储起来
// 注意:存储的是 callback 函数实例,不是它的返回值
hook.memoizedState = [nextCallback, nextDeps];
// console.log(`[mountCallback] Stored state: [Function, Deps]`);
return nextCallback; // 返回该 callback
}
/**
* Update 阶段的 useCallback 实现。
* @param {Function} callback 需要被 memoize 的回调函数
* @param {Array<any> | null | undefined} deps 依赖项数组
*/
function updateCallback(callback, deps) {
const hook = resolveCurrentlyProcessingHook(); // 获取当前 Hook 存储节点
// console.log(`[updateCallback] Hook Index: ${hookIndex}. Previous state saved.`);
if (!hook || !hook.memoizedState) {
console.error("[updateCallback] Error: Hook state not found during update. Re-creating callback.");
return mountCallback(callback, deps); // 借用 mount 逻辑(非标准行为)
}
const nextDeps = deps === undefined ? null : deps; // 标准化新的依赖项
const [prevCallback, prevDeps] = hook.memoizedState; // 获取上次存储的 callback 和依赖项
// 核心逻辑:比较新旧依赖项
if (areDepsEqual(nextDeps, prevDeps)) {
// 如果依赖项没有变化,直接返回上次存储的 callback 函数实例
console.log(`[updateCallback] Dependencies haven't changed. Reusing previous callback instance.`);
return prevCallback;
} else {
// 如果依赖项变化了,使用本次渲染传入的新的 callback 函数实例
console.log(`[updateCallback] Dependencies changed. Using new callback instance. Prev Deps:`, prevDeps, `Next Deps:`, nextDeps);
const nextCallback = callback;
// 更新 Hook 节点上存储的 callback 和依赖项
hook.memoizedState = [nextCallback, nextDeps];
console.log(`[updateCallback] Stored new state: [New Function, New Deps]`);
return nextCallback; // 返回新的 callback 实例
}
}
// ============================================================
// 模拟 React 对外暴露的 Hooks API
// ============================================================
// 这些是我们在组件中实际调用的 React.useMemo 和 React.useCallback
// 它们的作用是根据当前 Dispatcher 调用对应的实现 (mount 或 update)
const React = {
useMemo: (create, deps) => {
// console.log("React.useMemo called");
const dispatcher = ReactCurrentDispatcher.current;
if (!dispatcher) {
throw new Error("Invalid hook call. Hooks can only be called inside the body of a function component.");
}
// 动态调用 dispatcher 上的 useMemo 方法
return dispatcher.useMemo(create, deps);
},
useCallback: (callback, deps) => {
// console.log("React.useCallback called");
const dispatcher = ReactCurrentDispatcher.current;
if (!dispatcher) {
throw new Error("Invalid hook call. Hooks can only be called inside the body of a function component.");
}
// 动态调用 dispatcher 上的 useCallback 方法
return dispatcher.useCallback(callback, deps);
},
// 模拟 useState 用于演示
useState: (initialState) => {
// console.log("React.useState called");
const dispatcher = ReactCurrentDispatcher.current;
if (!dispatcher) {
throw new Error("Invalid hook call. Hooks can only be called inside the body of a function component.");
}
return dispatcher.useState(initialState);
}
};
// 模拟 useState 的 mount 和 update 实现 (非常简化)
function mountState(initialState) {
const hook = resolveCurrentlyProcessingHook();
console.log(`[mountState] Hook Index: ${hookIndex}. Initializing state.`);
let state;
if (typeof initialState === 'function') {
state = initialState();
} else {
state = initialState;
}
hook.memoizedState = state;
const setState = (newState) => {
// 简化:直接修改 state,并触发重新渲染(真实 React 有复杂的更新队列)
console.log(`[setState] Updating state for Fiber ${currentlyRenderingFiber?.type.name}. New state:`, newState);
// 假设这个 setState 会调度一次新的渲染
// 在这个模拟中,我们手动调用 renderRoot 来模拟更新
};
return [hook.memoizedState, setState];
}
function updateState(initialState) {
const hook = resolveCurrentlyProcessingHook();
// console.log(`[updateState] Hook Index: ${hookIndex}. Reusing previous state:`, hook.memoizedState);
const setState = (newState) => {
console.log(`[setState] Updating state for Fiber ${currentlyRenderingFiber?.type.name}. New state:`, newState);
// 简化处理:直接更新 hook 状态,真实情况更复杂
hook.memoizedState = typeof newState === 'function' ? newState(hook.memoizedState) : newState;
// 触发重渲染的逻辑省略...
};
return [hook.memoizedState, setState];
}
// ============================================================
// 示例:模拟一个函数组件的渲染过程
// ============================================================
let renderCount = 0;
let componentState = 0; // 外部模拟的状态,用于触发更新
// 模拟的函数组件
function MyComponent(props) {
renderCount++;
console.log(`\n--- Rendering MyComponent (render #${renderCount}, state: ${componentState}) ---`);
// 假设有一个 state 触发更新
// const [count, setCount] = React.useState(0); // 真实用法
// 这里我们用外部变量 componentState 模拟
// 示例 1: useMemo - 只有当 componentState 变化时才重新计算 expensiveValue
const expensiveValue = React.useMemo(() => {
console.log(" [useMemo] Executing expensive calculation...");
let sum = 0;
for (let i = 0; i < (componentState + 1) * 10; i++) { // 计算量依赖于 state
sum += i;
}
console.log(" [useMemo] Calculation finished.");
return sum;
}, [componentState]); // 依赖项是 componentState
console.log(` Received expensiveValue: ${expensiveValue}`);
// 示例 2: useMemo - 传入空数组 [], 只在 Mount 时计算一次
const initialTimestamp = React.useMemo(() => {
console.log(" [useMemo] Calculating initial timestamp (should run only once)...");
return Date.now();
}, []); // 空依赖数组
console.log(` Received initialTimestamp: ${initialTimestamp}`);
// 示例 3: useCallback - 只有当 componentState 变化时才创建新的 handleLog 函数实例
const handleLog = React.useCallback(() => {
console.log(` [Callback Executed] Current expensiveValue: ${expensiveValue}, State: ${componentState}`);
// 这个回调函数本身很简单,但如果它被传递给子组件作为 prop,
// useCallback 可以防止因为父组件渲染导致子组件不必要的重新渲染(如果子组件用了 React.memo)
}, [componentState, expensiveValue]); // 依赖项 list 包含了 componentState 和 expensiveValue
console.log(` Received handleLog function instance.`);
// 示例 4: useCallback - 传入空数组 [], 只在 Mount 时创建一次函数实例
const handleMountLog = React.useCallback(() => {
console.log(" [Callback Executed] This log function was created on mount.");
}, []); // 空依赖数组
console.log(` Received handleMountLog function instance.`);
// 模拟返回 JSX (这里只是返回一个描述)
return `Component Render #${renderCount}`;
}
// --- 模拟渲染流程 ---
// 1. 首次渲染 (Mount)
console.log(">>> Simulating Initial Mount <<<");
const myFiber = new FiberNode('FunctionComponent', MyComponent);
// 设置为 Mount dispatcher
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
renderRoot(myFiber);
// 打印首次渲染后的 Fiber 状态 (Hooks 链表)
console.log("\nFiber state after initial mount:", JSON.stringify(myFiber.memoizedState, (key, value) => {
if (typeof value === 'function') return '[Function]';
return value;
}, 2));
// 2. 模拟一次状态更新,触发重新渲染 (Update)
console.log("\n>>> Simulating Update (state changes) <<<");
componentState = 1; // 改变状态
// 设置为 Update dispatcher
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
renderRoot(myFiber); // 再次渲染同一个 Fiber
// 打印更新后的 Fiber 状态
console.log("\nFiber state after update:", JSON.stringify(myFiber.memoizedState, (key, value) => {
if (typeof value === 'function') return '[Function]';
return value;
}, 2));
// 3. 模拟另一次状态不变的渲染 (Update)
// 例如,父组件重新渲染,但传递给 MyComponent 的 props 没有变,并且 MyComponent 自身状态也没变
// 但在这个简化模拟中,我们直接调用 renderRoot,相当于强制渲染
console.log("\n>>> Simulating Update (state remains the same) <<<");
// componentState 保持为 1
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
renderRoot(myFiber);
// 打印更新后的 Fiber 状态 (应该和上一次一样)
console.log("\nFiber state after second update:", JSON.stringify(myFiber.memoizedState, (key, value) => {
if (typeof value === 'function') return '[Function]';
return value;
}, 2));
// 4. 模拟执行 memoized 回调
console.log("\n>>> Simulating Callback Execution <<<");
// 获取最后一次渲染得到的 handleLog 函数实例
const lastHandleLog = myFiber.memoizedState // hook 0 (useMemo)
.next // hook 1 (useMemo [])
.next.memoizedState[0]; // hook 2 (useCallback) -> [callback, deps] -> callback
lastHandleLog();
const lastHandleMountLog = myFiber.memoizedState // hook 0 (useMemo)
.next // hook 1 (useMemo [])
.next // hook 2 (useCallback)
.next.memoizedState[0]; // hook 3 (useCallback []) -> [callback, deps] -> callback
lastHandleMountLog();
// --- 再次状态更新 ---
console.log("\n>>> Simulating Another Update (state changes again) <<<");
componentState = 2; // 再次改变状态
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
renderRoot(myFiber);
console.log("\nFiber state after third update:", JSON.stringify(myFiber.memoizedState, (key, value) => {
if (typeof value === 'function') return '[Function]';
return value;
}, 2));
代码讲解概要:
-
模拟环境:
currentlyRenderingFiber
,workInProgressHook
,hookIndex
: 模拟 React 内部追踪当前渲染状态的变量。FiberNode
,HookNode
: 极度简化的 Fiber 和 Hook 节点类,用来存储状态 (memoizedState
) 和 Hook 链表结构 (next
)。真实结构复杂得多。renderRoot
: 模拟 React 发起组件渲染的入口函数,负责设置全局状态、调用组件函数、清空状态。
-
Dispatcher 机制:
HooksDispatcherOnMount
,HooksDispatcherOnUpdate
: 两个对象,分别包含了 Hooks 在首次渲染和更新时的具体实现函数。ReactCurrentDispatcher
: 一个包含current
属性的对象,React 在渲染组件前会将其指向 Mount 或 Update Dispatcher。resolveCurrentlyProcessingHook
: 关键的辅助函数,模拟 React 如何根据hookIndex
找到或创建当前 Hook 对应的存储节点 (HookNode
)。它处理了 Hook 链表的创建(Mount 时)和遍历(Mount 和 Update 时)。真实实现更复杂,涉及错误处理(如 Hook 顺序改变)。
-
areDepsEqual
依赖项比较:- 这是
useMemo
和useCallback
的核心判断逻辑。 - 处理
deps
为null
或undefined
的情况(视为永不相等)。 - 比较数组长度。
- 使用
Object.is()
逐个比较依赖项。注释解释了为何使用Object.is
而不是===
。 - 重要性: 正确理解依赖项比较是使用这两个 Hook 的关键,错误的依赖项(比如每次渲染都创建新数组/对象作为依赖)会导致 memoization 失效。
- 这是
-
useMemo
实现 (mountMemo
,updateMemo
) :mountMemo
:- 获取 Hook 节点。
- 标准化
deps
。 - 直接调用
create
函数计算值。 - 将
[value, deps]
存储在hook.memoizedState
中。 - 返回计算值。
updateMemo
:- 获取 Hook 节点。
- 从
hook.memoizedState
取出上次存储的[prevState, prevDeps]
。 - 调用
areDepsEqual
比较nextDeps
和prevDeps
。 - 如果相等: 直接返回
prevState
,不执行create
函数。 - 如果不相等: 调用
create
函数计算nextValue
,将[nextValue, nextDeps]
存入hook.memoizedState
,返回nextValue
。
- 注释详细解释了每一步的操作和原因。
-
useCallback
实现 (mountCallback
,updateCallback
) :mountCallback
:- 获取 Hook 节点。
- 标准化
deps
。 - 直接使用传入的
callback
函数实例。 (注意:不是调用它!) - 将
[callback, deps]
存储在hook.memoizedState
。 - 返回
callback
实例。
updateCallback
:- 获取 Hook 节点。
- 取出上次存储的
[prevCallback, prevDeps]
。 - 调用
areDepsEqual
比较nextDeps
和prevDeps
。 - 如果相等: 返回
prevCallback
(上一次的函数实例)。 - 如果不相等: 使用本次传入的
callback
实例作为nextCallback
,将[nextCallback, nextDeps]
存入hook.memoizedState
,返回nextCallback
。
- 强调了
useCallback
是 memoize 函数实例本身,而useMemo
是 memoize 函数的执行结果。
-
模拟 React API (
React.useMemo
,React.useCallback
,React.useState
) :- 这些是我们平时在代码中写的
React.useMemo
等。 - 它们的核心作用就是读取
ReactCurrentDispatcher.current
,然后调用其上对应的 Hook 方法(useMemo
或useCallback
)。这使得同一段React.useMemo(...)
代码能在 Mount 和 Update 时表现不同。 - 添加了简化的
useState
及其mountState
/updateState
来让示例组件能工作。
- 这些是我们平时在代码中写的
-
示例组件与渲染模拟 :
MyComponent
: 一个简单的函数组件,使用了useMemo
和useCallback
。expensiveValue
: 使用useMemo
缓存一个计算结果,依赖于模拟的componentState
。initialTimestamp
: 使用useMemo
和空依赖数组[]
,只在首次渲染时计算一次。handleLog
: 使用useCallback
缓存一个回调函数,依赖于componentState
和expensiveValue
。handleMountLog
: 使用useCallback
和空依赖数组[]
,只在首次渲染时创建一次函数实例。
- 模拟渲染流程:
- 创建
FiberNode
。 - 首次 Mount: 设置
ReactCurrentDispatcher
为HooksDispatcherOnMount
,调用renderRoot
。观察控制台输出,useMemo
的计算函数和useCallback
的创建逻辑都被执行。 - 首次 Update (state 改变): 改变
componentState
,设置 Dispatcher 为HooksDispatcherOnUpdate
,再次调用renderRoot
。观察输出:- 依赖于
componentState
的useMemo
(expensiveValue) 重新计算。 - 空依赖的
useMemo
(initialTimestamp) 没有重新计算,返回了缓存值。 - 依赖于
componentState
的useCallback
(handleLog) 创建了新的函数实例。 - 空依赖的
useCallback
(handleMountLog) 复用了旧的函数实例。
- 依赖于
- 第二次 Update (state 不变): 保持
componentState
不变,再次渲染。观察输出:- 所有
useMemo
和useCallback
都因为依赖项未变而复用了上一次的结果/实例,它们的计算函数/创建逻辑不再执行。
- 所有
- 模拟回调执行: 获取最后一次渲染得到的
useCallback
实例并执行它们,验证其行为。 - 第三次 Update (state 再次改变): 再次改变
componentState
并渲染,验证行为是否符合预期。
- 创建
- 打印 Fiber state: 在每次渲染后打印
myFiber.memoizedState
的内容(简化展示),可以看到 Hook 链表以及每个 Hook 存储的状态([value/callback, deps]
)。
核心原理总结:
- 状态存储: 每个
useMemo
或useCallback
调用在组件的 Fiber 节点内部都有一个对应的 Hook 数据结构(模拟中的HookNode
),用于存储上一次计算/创建的值(或函数实例)以及当时的依赖项数组。 - 渲染时机区分: React 通过 Dispatcher 机制,在组件首次渲染(Mount)和后续更新(Update)时执行不同的 Hook 逻辑。
- Mount 阶段: 直接计算值(
useMemo
)或使用当前函数实例(useCallback
),并将结果和依赖项存入 Hook 状态。 - Update 阶段:
- 取出上次存储的值/函数和依赖项。
- 获取本次渲染传入的新的依赖项。
- 关键: 使用
areDepsEqual
(内部基于Object.is
)浅比较新旧依赖项数组。 - 依赖项未变: 返回上次存储的值/函数实例,跳过计算/创建过程。这就是性能优化的来源。
- 依赖项改变: 重新计算值(
useMemo
)或使用本次传入的函数实例(useCallback
),并将新的结果和新的依赖项存入 Hook 状态,然后返回新结果。
useCallback(fn, deps)
是useMemo(() => fn, deps)
的语法糖: 它俩的底层机制几乎完全一样,只是useCallback
专门用于 memoize 函数实例,而useMemo
用于 memoize 任何计算结果。