React 并不会通过“变量名”来记住组件的状态,而是通过“调用顺序”和“链表结构”来对应的。
核心在于其独特的数据结构和执行规则,使得函数组件能够“记住”状态和副作用。
-
- 数据结构 :每个组件实例都关联着一个链表,用户储存所有的 Hooks 的状态
-
- 执行规则 : Hooks 必须按照固定的顺序,以此来确保状态与 HOOK 的一一对应
一、核心结构
在 React 内部,每个函数都对应一个 Fiber 节点 。这个节点(可以看作是组件的“骨架”或实例信息)上有一个 memoizedState 属性,它指向一个单向链表 ,这个链表就是储存所有 Hooks 状态的地方。
链表中的每一个节点(一个 HOOK 对象)都包含一下关键信息:
memoizedState:储存当前 Hook 的状态值(例如useState的值、useEffect的回调函数和依赖 )next:指向下一个 Hook 节点的指针,将所有 Hooks 串联起来queue:储存该 Hook 的更新列队(例如useState的setState触发的更新)
Hook 对象的结构大致如下:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // 当前状态值(对于 useState ) 或依赖项(对于 useEffect)
baseState: null, // 基础状态(用于计算更新)
baseQueue: null, // 原始
queue: null, // 更新列队(储存 dispatchAction 产生的 update)
next: null, // 指向下一个 Hook 的指针(关键!)
};
if (workInProgressHook === null) {
// This is the first hook in the list
// 这是列表中的第一个钩子
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// 添加到列表末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
链表允许 React 在组件渲染时,通过简单的指针移动( currentHook = current.next )来按顺序访问状态,而不需要像数组那样在中间插入元素时进行大量的索引移动。
1. 状态是怎么被记住的
这个过程分为两个阶段:
-
- 首次渲染 :当组件首次渲染时, React 会从上到下执行函数组件的代码。每当遇到一个 Hook (如
useState), React 就会创建(通过 mountWorkInProgressHook 方法)一个新的 Hook 节点,将其添加到Fiber节点的 Hook 链表莫问。并初始化它的memoizedState
- 首次渲染 :当组件首次渲染时, React 会从上到下执行函数组件的代码。每当遇到一个 Hook (如
-
- 更新重渲染 :当组件状态改变时触发重新渲染时,函数组件会再次执行。此时, React 不会再创建新的 Hook 节点,而是按照与首次渲染完全相同的顺序,遍历已经存在的 Hook 链表,依次读取每个节点储存的
memoizedState值
- 更新重渲染 :当组件状态改变时触发重新渲染时,函数组件会再次执行。此时, React 不会再创建新的 Hook 节点,而是按照与首次渲染完全相同的顺序,遍历已经存在的 Hook 链表,依次读取每个节点储存的
二、为什么 Hook 不能写在条件语句中
根源就在于链表结构和顺序依赖。
React 依靠 Hook 的调用顺序来确保每次渲染时,其状态能正确对应到具体的 Hook 上。
一旦顺序发生改变, React 就无法正确匹配状态和 Hook ,导致数据错乱或崩溃。
1. 以 useState 为例
useState 的工作原理完美展示了 React 的 Hook 机制:
-
- 读取状态 :当调用
useState(initialValue)时, React 会根据当前的调用顺序,在 HOOK 链表中构建一个新的 Hook 并添加状态到hook.memoizedState及hook.baseState
初始化 useState 实现方法function mountState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {const hook = mountStateImpl(initialState);const queue = hook.queue;const dispatch: Dispatch<BasicStateAction<S>> = dispatchSetState.bind(null,currentlyRenderingFiber,queue,) as any;queue.dispatch = dispatch;return [hook.memoizedState, dispatch];}function mountStateImpl<S>(initialState: (() => S) | S): Hook {const hook = mountWorkInProgressHook();if (typeof initialState === 'function') {const initialStateInitializer = initialState;initialState = initialStateInitializer();if (shouldDoubleInvokeUserFnsInHooksDEV) {setIsStrictModeForDevtools(true);try {initialStateInitializer();} finally {setIsStrictModeForDevtools(false);}}}hook.memoizedState = hook.baseState = initialState;const queue: UpdateQueue<S, BasicStateAction<S>> = {pending: null,lanes: NoLanes,dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: initialState as any,};hook.queue = queue;return hook;} - 读取状态 :当调用
-
- 更新状态 :当调用
setState(newValue)时, React 并不会立即修改那个状态。它会将这个更新请求放入对应的 Hook 节点的queue(更新列队)中,然后出发组件的重新渲染
- 更新状态 :当调用
-
- 处理更新 :在重新渲染阶段, React 会处理
queue中的所有更新,计算出最新的memoizedState值,并在下一次渲染时返回
- 处理更新 :在重新渲染阶段, React 会处理
三、惰性初始 State(Lazy Initial State)
当使用 useState(() => expensiveComputation()) 时,React 并不会在每次渲染都执行这个函数。
实现逻辑:
在 mount 阶段,useState 会检查传入的参数。
-
- 如果参数时一个函数(惰性初始化), React 会立即执行它一次,得到初始值
-
- 将这个值存入 Hook 对象的
memoizedState和baseState
- 将这个值存入 Hook 对象的
-
- 关键点 : 在后续的更新( Update )阶段, React 不再关心初始值,而是直接读取
memoizedState或基于baseState计算新值
- 关键点 : 在后续的更新( Update )阶段, React 不再关心初始值,而是直接读取
四、dispatchAction 与更新列队
当调用 setState (即 dispatchAction )时,React 并没有立即修改状态,而是采用了“入队”的策略。
dispatchAction 的工作流程:
-
- 创建 Update 对象 :包含要更新的动作( action )、优先级( lane )等信息
-
- 插入循环链表 :每个 Hook 都有一个
queue,它是一个循环链表。dispatchAction会将新的 Update 对象插入到这个链表中
- 结构类似于:
Update1 ➞ Update2 ➞ Update3 ➞ (回到头)
- 插入循环链表 :每个 Hook 都有一个
-
- 调度更新 :触发 React 的调度机制,安排组件重新渲染
在渲染阶段( Update Reducer ) :
React 会遍历这个 queue 链表,依次计算所有的更新( reducer ),计算出最新的 memoizedState ,然后返回
五、 effectTag 标记(插入/更新/删除)
虽然 Hooks 本身是链表结构,但 React 如果知道该对该副作用( Effect )做什么操作(增删改)呢?这依赖于 Fiber 节点的 flags (旧版本称为 effectTag )。
在 Reconciliation (调和)阶段, React 会对比新旧 Hooks 链表: