Appearance
第 2.3 节:Hooks 的基石:useState 与 useEffect
自 React 16.8 引入以来,Hooks 彻底改变了我们编写 React 组件的方式。它允许我们在函数组件中使用 state 和其他 React 特性,而无需编写 class。但 Hooks 背后并非魔法,而是一套依赖于 Fiber 架构和“调用顺序”的精巧设计。
本节将深入源码,揭示 useState 和 useEffect 的工作原理。
2.3.1 Hook 的数据结构与链表
Hooks 的核心秘密在于,它们以链表的形式存储在与特定函数组件关联的 Fiber 节点上。每次函数组件渲染时,它都会按照固定的顺序访问这个链表,从而获取或更新每个 Hook 的状态。
文件定位:packages/react-reconciler/src/ReactFiberHooks.js
首先,我们来看 Hook 对象的定义:
javascript
export type Hook = {
memoizedState: any, // 当前 Hook 的状态值 (e.g., a state from useState)
baseState: any, // 用于计算下一个 state 的基础 state
baseQueue: Update<any, any> | null, // 待处理的更新队列的子集
queue: any, // 完整的更新队列 (UpdateQueue)
next: Hook | null, // 指向下一个 Hook 对象的指针
};关键点在于 next 属性。正是这个属性将组件内所有 Hooks 串成一个单向链表。这个链表的头节点存储在 Fiber 节点的 memoizedState 属性上。
Fiber.memoizedState: 对于函数组件,如果它使用了 Hooks,
memoizedState存储的不是组件的状态,而是其 Hooks 链表的第一个 Hook 对象。
这就是为什么我们必须在组件的顶层调用 Hooks,并且不能在循环、条件或嵌套函数中调用它们。React 依赖于每次渲染时 Hooks 完全相同的调用顺序来正确地从链表中获取每个 Hook 对应的状态。
2.3.2 Dispatcher 对象的作用
你可能想知道,我们从 react 包中导入的 useState 函数,是如何与特定组件的 Fiber 节点关联起来的?答案是 Dispatcher。
React 内部有一个全局的“指针”,名为 ReactCurrentDispatcher。在 React 开始渲染一个函数组件之前,它会将这个指针指向一个包含了所有 Hook 实现的对象(Dispatcher)。
文件定位:shared/ReactSharedInternals.js
javascript
const ReactSharedInternals = {
ReactCurrentDispatcher,
// ...
};这个 Dispatcher 对象有两种版本:
- Mount-Dispatcher:在组件首次渲染(mount)时使用。此时的
useState会创建并初始化 Hook 对象。 - Update-Dispatcher:在组件后续更新(update)时使用。此时的
useState会从链表中读取并返回当前 Hook 的状态。
在渲染函数组件之前,React 会调用 renderWithHooks 函数,该函数会根据当前是 mount 还是 update,来设置正确的 Dispatcher。
javascript
// packages/react-reconciler/src/ReactFiberHooks.js
// 在调用组件函数之前设置
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null; // 清空旧的 hooks 链表
workInProgress.updateQueue = null;
// 根据是 mount 还是 update,选择不同的 dispatcher
if (current !== null && current.memoizedState !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
}
// ... 然后调用你的函数组件
const children = Component(props, secondArg);当我们调用 useState() 时,实际上是在调用 ReactCurrentDispatcher.current.useState(),从而执行了与当前渲染阶段(mount 或 update)相对应的正确逻辑。
useState 的实现
useState 的核心逻辑分为 mountState 和 updateState。
mountState (首次渲染)
- 创建一个新的
Hook对象。 - 初始化
memoizedState为传入的初始值。 - 创建一个
UpdateQueue,其中包含dispatch函数(即我们用来更新状态的setState)。 - 将这个新的
Hook对象追加到当前 Fiber 的 Hooks 链表末尾。 - 返回
[memoizedState, dispatch]。
updateState (更新渲染)
- 从 Hooks 链表中获取当前的
Hook对象。 - 处理
UpdateQueue中的所有更新,计算出新的memoizedState。 - 如果 state 没有变化,则可以进行优化,提前退出。
- 返回
[memoizedState, dispatch]。
useEffect 的实现
useEffect 的工作方式类似,但它处理的是“副作用”。它的核心是创建一个 Effect 对象。
文件定位:packages/react-reconciler/src/ReactFiberHooks.js
javascript
export type Effect = {
tag: HookFlags, // 标记 effect 类型 (e.g., Passive, Layout)
inst: EffectInstance, // 存储 destroy 函数
create: () => (() => void) | void, // effect 的回调函数
deps: Array<mixed> | void | null, // 依赖项数组
next: Effect, // 指向下一个 Effect 对象
};创建 Effect 对象:当你调用
useEffect(create, deps)时,React 会创建一个Effect对象,并将其存储在Hook对象的memoizedState中。推入更新队列:同时,这个
Effect对象会被添加到一个特殊的链表中,该链表挂载在函数组件 Fiber 节点的updateQueue上。这个队列专门用来处理useEffect、useLayoutEffect等副作用钩子。javascript// FunctionComponentUpdateQueue { lastEffect: Effect | null, // ... }标记 Fiber 节点:React 会在当前 Fiber 节点上添加一个
flags(副作用标记),例如Passive或Update。这告诉 React 在 Commit 阶段需要处理这个组件的副作用。Commit 阶段执行:在 Commit 阶段,React 会遍历 Fiber 树,寻找带有副作用
flags的节点。找到后,它会检查该节点的updateQueue,并执行其中的Effect链表。- 对于
useEffect(带有Passive标记),其create函数会在浏览器完成绘制后异步执行。 - 对于
useLayoutEffect(带有Layout标记),其create函数会在所有 DOM 变更后、浏览器绘制前同步执行。
- 对于
设计思想解读:
- 分离状态与副作用:Hooks 将组件的状态(
useState)和副作用(useEffect)逻辑清晰地分离。状态计算在 Render 阶段完成,而副作用的执行则被推迟到 Commit 阶段。 - 依赖数组优化:通过比较
deps数组,React 可以决定是否需要重新执行useEffect的create函数,避免了不必要的计算和 DOM 操作。 - 与 Fiber 协同:Hooks 的实现与 Fiber 的工作流紧密耦合。Render 阶段创建
Hook和Effect对象,并用flags标记 Fiber;Commit 阶段根据flags执行相应的副作用,完美融入了 React 的异步渲染管线。