理解了 Hooks 的闭包本质和链表存储结构后,我们就能深入剖析 React 中最常用的三个 Hooks——useState、useEffect 和 useMemo/useCallback 的底层工作机制。这些原理直接决定了它们在代码中的行为表现,理解它们能让你避开大量隐性 bug。
7.3.1 useState:状态存储与触发更新
useState 是函数组件保持“记忆”的关键。当组件首次渲染时,useState 会在对应的 Hook 节点上初始化一个状态槽,并将初始值存入 hook.memoizedState。在后续渲染中,React 直接从这个槽位读出当前状态。
状态更新并不仅仅是修改内存中的值——它还涉及更新队列和调度重渲染。
简化版的内部流程
// 伪代码:React 内部对 useState 的抽象实现
function useState(initialValue) {
const hook = getOrCreateWorkInProgressHook(); // 获取当前 Hook 节点
// 首次渲染
if (hook.memoizedState === undefined) {
hook.memoizedState = typeof initialValue === 'function'
? initialValue()
: initialValue;
}
// dispatch 函数,用于更新状态
const dispatch = (action) => {
// 计算新状态(支持函数式更新)
const newState = typeof action === 'function'
? action(hook.memoizedState)
: action;
// 放入更新队列
hook.queue.pending = {
action: newState, // 简化:实际存储的是 update 对象
next: null
};
// 触发重新渲染
scheduleUpdate();
};
return [hook.memoizedState, dispatch];
}
真正的实现远比这个复杂,主要包含以下细节:
- 惰性初始化:如果传给
useState的是一个函数,React 只会在首次渲染时调用它来获取初始值,后续渲染不再执行该函数。这可以避免昂贵的初始计算。
- 更新队列处理:在多次调用
dispatch时(特别是同一个事件处理函数中连续调用多个setState),React 不会立即刷新状态,而是将更新对象(update)依次放入队列。在正式进入渲染阶段时,React 会遍历队列,依次应用每个更新(如果是函数,会传入前一个状态),最终计算出最新状态存入memoizedState。
- 批量更新(Batching):在 React 18 之前,只有合成事件和生命周期中的
setState会被批量处理;异步回调(如setTimeout)中的更新会立即执行。React 18 通过并发模式实现了自动批量更新,任何场景下的多个setState都会被合并到一次渲染中。
- 闭包陷阱的本质:由于
useState返回的状态值是每一次渲染快照中的常值(捕获了当前渲染周期的闭包变量),若在异步回调中使用状态的旧值,就会出现“状态滞后”问题。解决方式是使用函数式更新(setState(prev => prev + 1)),它不依赖闭包中的旧值,而是基于 React 内部保证的最新状态。
示例:函数式更新避免闭包陷阱
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// ❌ count 始终是首次渲染时的 0
// setCount(count + 1);
// ✅ 使用函数式更新,pre 是最新状态
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖意味着只在挂载时执行一次
return <h1>{count}</h1>;
}
7.3.2 useEffect:副作用收集与调度
useEffect 的本质是把副作用函数(及其依赖)登记到当前 Fiber 节点的 updateQueue 中,然后由 React 在合适的时机统一执行。
收集阶段(Render 阶段)
在函数组件执行(渲染)期间,调用 useEffect 时会创建一个 effect 对象:
{
tag: HookFlags.Effect, // 标记为 effect
create: () => { /* 副作用函数 */ },
destroy: undefined, // 清理函数,在下次 effect 执行前调用
deps: [dep1, dep2], // 依赖项数组
next: null // 链表指针
}
这个对象被挂载到 hook.memoizedState 上(实际上 useEffect 对应的 Hook 存储的就是一个 effect 链表)。在完成整个组件的渲染后,React 将收集到的所有 effect 放入当前 Fiber 的 updateQueue。
执行阶段(Commit 阶段)
真正执行副作用是在 DOM 更新之后(commit 阶段)。React 会根据 effect 的类型(useEffect 是异步非阻塞的,而 useLayoutEffect 是同步的)决定执行时机:
useEffect:在浏览器将变更绘制到屏幕之后异步执行,不会阻塞页面视觉更新。useLayoutEffect:在 DOM 变更后浏览器绘制之前同步执行,用于需要同步读取布局信息的场景。
对于每个 effect,React 会先检查依赖项是否变化:
function areDepsEquals(prevDeps, nextDeps) {
for (let i = 0; i < prevDeps.length; i++) {
if (Object.is(prevDeps[i], nextDeps[i])) continue;
return false;
}
return true;
}
比较算法是 Object.is,而非浅比较或深比较。这意味:
NaN和NaN被视为相等(Object.is(NaN, NaN)为true)。0和-0被视为不相等。- 引用类型(对象、数组、函数)只有在引用不变时才算相等。
如果依赖项发生了变化,React 会先调用前一次 effect 的清理函数(destroy),再执行本次的 create 函数;如果没有变化,则完全跳过。
清理函数的执行时机
- 在依赖变化时,下一次 effect 执行前会先运行上一次的清理函数。
- 组件卸载时,React 会运行最后一次 effect 的清理函数,避免内存泄漏。
useEffect(() => {
const subscription = subscribe(props.id);
return () => {
// 在 props.id 变化或组件卸载时执行
subscription.unsubscribe();
};
}, [props.id]);
7.3.3 useMemo 与 useCallback:值的缓存与函数的稳定引用
这两个 Hook 本质上是同一类优化手段:基于依赖项的缓存。它们都在渲染阶段执行,如果依赖项没有变化,则直接返回上一次的缓存结果。
useMemo:缓存计算结果
function useMemo(create, deps) {
const hook = getOrCreateWorkInProgressHook();
// 获取上一次的缓存值和依赖项
const prevState = hook.memoizedState;
if (prevState !== undefined) {
const [prevValue, prevDeps] = prevState;
if (areDepsEquals(prevDeps, deps)) {
return prevValue; // 依赖未变,返回原值
}
}
// 重新计算并缓存
const nextValue = create();
hook.memoizedState = [nextValue, deps];
return nextValue;
}
useMemo 适合用在计算开销较大且依赖项不频繁变化的场景,如复杂的数据派生。但注意,它本身也有比较依赖项的开销,对于轻量计算可能得不偿失。
useCallback:缓存函数引用
useCallback 的实现与 useMemo 完全一致,只是返回值不同:
function useCallback(callback, deps) {
return useMemo(() => callback, deps);
}
它的核心价值在于保持函数引用稳定,从而避免子组件不必要的重渲染(当子组件被 React.memo 包裹且依赖函数引用时)。
误区与正确使用
- 不要无差别地用
useCallback/useMemo包裹所有函数和值:依赖项比较和缓存本身也有成本,对于轻量计算或几乎总是变化的依赖,缓存起不到优化作用,还增加了代码复杂度。 - 依赖项必须真实完整:如果在
useCallback内部使用了某个状态或 props,却未将其加入依赖数组,React 仍会使用旧的闭包值,导致逻辑错误。ESLint 的exhaustive-deps规则正是为此而生。
通过理解这三个核心 Hooks 的底层原理,你会发现 React 的 Hooks 体系并非黑魔法,而是一套严谨的闭包 + 链表 + 有序调度机制。掌握这些知识后,像闭包陷阱、无限循环、性能优化失效等问题都将变得透明可控。