Hooks 初始化
知道了执行函数组件的函数,以及不同的 Hooks 对象之后,我们看一下在组件初始化的时候,
Hooks 的处理逻辑,Hooks 初始化流程使用的是 mountState、mountEffect 等初始化节点的 Hooks,将Hooks 和 fiber 建立起联系,那么如何建立起关系呢,每一个 Hooks 初始化都会执行 mountWorkInProgressHook,接下来看一下这个函数。
js
function mountWorkInProgressHook() {
const hook: Hook = {
memoizedState: null,
// useState 中保存着 state 信息 | useEffect 中保存着 effect 对象 | useMemo 中保存的是缓存的值和deps | useRef 中保存的是 ref 对象 baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook = = = null) {
// 例子中的第一个 hooks→ useState(0) 走的就是这样。
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
1. Hook对象初始化(mountWorkInProgressHook)
首先,每个Hook都是一个对象,其结构如下:
javascript
const hook = {
memoizedState: null, // 存储Hook的具体状态
baseState: null, // 用于更新队列的基础状态(主要用于useState/useReducer)
baseQueue: null, // 未处理的更新队列(主要用于useState/useReducer)
queue: null, // 更新队列(保存待处理的更新和dispatch函数)
next: null, // 指向下一个Hook,形成链表
};
在函数组件中,多个Hook通过
next指针形成链表,而fiber节点的
memoizedState指向这个链表的头节点。
2. 三种Hook的初始化细节
a) useState
初始化useState
会调用mountState
:
javascript
function mountState(initialState) {
const hook = mountWorkInProgressHook(); // 创建一个新的Hook对象
hook.memoizedState = hook.baseState =
typeof initialState === 'function' ? initialState() : initialState;
const queue = {
pending: null, // 待处理的更新队列(循环链表)
dispatch: null, // 更新函数
lastRenderedReducer: basicStateReducer, // 用于计算状态的reducer
lastRenderedState: hook.memoizedState, // 上一次渲染的状态
};
hook.queue = queue;
const dispatch = (queue.dispatch = dispatchAction.bind(
null,
currentlyRenderingFiber,
queue
));
return [hook.memoizedState, dispatch];
}
-
状态存储 :初始状态存储在
hook.memoizedState
和hook.baseState
。 -
更新队列 :创建一个队列对象,用于存储后续的更新(如多次调用
setState
)。 -
dispatch函数 :返回的更新函数(
setNumber
)被绑定到当前fiber和队列上,确保更新能正确调度。
2. dispatchAction
更新机制
当我们调用 setNumber
时,实际上是在调用 dispatchAction
函数。它的核心逻辑如下:
js
function dispatchAction(fiber, queue, action) {
// 1. 获取更新优先级(Lane模型)
const lane = requestUpdateLane(fiber);
// 2. 创建更新对象
const update = {
lane,
action, // 更新函数或值
hasEagerState: false, // 是否已计算急切的state
eagerState: null, // 计算出的state
next: null, // 指向下一个更新
};
// 3. 将更新加入队列
if (isRenderPhaseUpdate(fiber)) {
// 如果当前 fiber 正在更新,就跳过本次更新,等到接下来一起更新就可以了
enqueueRenderPhaseUpdate(queue, update);
} else {
// 常规更新
enqueueUpdate(fiber, queue, update, lane);
// 4. 性能优化:提前计算新状态
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 获取当前状态
const currentState = queue.lastRenderedState;
const eagerState = lastRenderedReducer(currentState, action);
// 保存计算结果
update.hasEagerState = true;
update.eagerState = eagerState;
// 5. 如果状态未变化,则跳过更新
if (is(eagerState, currentState)) {
return;
}
}
// 6. 调度更新
const eventTime = requestEventTime();
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}
3. 核心流程解析
a) 更新对象(Update)结构
每次调用 setState
都会创建一个更新对象:
javascript
const update = {
lane: Lane, // 更新优先级
action: Function|any, // 更新函数或新状态值
hasEagerState: false, // 是否已预计算
eagerState: any, // 预计算的状态
next: Update|null // 指向下一个更新
};
b) 更新队列(Queue)结构
每个 Hook 的更新对象会组成一个环形链表:
javascript
const queue = {
pending: Update, // 指向最后一个更新(形成环形)
dispatch: Function, // setState 函数
lastRenderedReducer: Function, // 状态计算函数
lastRenderedState: any // 上一次渲染的状态值
};
c) 更新入队逻辑
javascript
function enqueueUpdate(queue, update) {
if (queue.pending === null) {
// 第一个更新:形成自环
update.next = update;
} else {
// 插入到环形链表尾部
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update; // 始终指向最新的更新
}
d) 环形链表示例

环形链表在React更新队列中的设计
注意:环形链表中,pending
指向最新更新(U3),而U3的next指向链表头部(U1),尾部(U2)指向U3。
合并队列时的优势
在更新过程中,React需要将当前未处理的更新(pending queue)合并到基础队列(baseQueue)中。这里,环形链表的优势在于可以高效地完成两个队列的合并。
为什么环形链表方便合并?
-
快速定位头尾:在环形链表中,通过一个节点(pending指针指向的节点)可以立即得到头(pending.next)和尾(pending)。
-
高效连接:合并两个环形链表只需要修改两个指针
实际例子
假设当前baseQueue是:B1 -> B2 -> B1 (环形,baseQueue指针指向B2)
pendingQueue是:U1 -> U2 -> U3 -> U1 (环形,pendingQueue指针指向U3)
合并后:
-
将baseQueue的尾(B2)与pendingQueue的头(U1)连接:B2.next = U1
-
将pendingQueue的尾(U3)与baseQueue的头(B1)连接:U3.next = B1
形成新环形链表:B1 -> B2 -> U1 -> U2 -> U3 -> B1
同时,baseQueue指针指向U3(即最后一个更新)。
这样,我们就将pendingQueue中的更新追加到了baseQueue的尾部。
疑问: dispatchAction 的性能优化与ensureRootIsScheduled 有何不同?
1. dispatchAction
层级:更新生产层
scss
function dispatchAction(fiber, queue, action) {
const update = createUpdate(action); // 1. 创建update
enqueueUpdate(queue, update); // 2. 加入队列
if (fiberNotUpdating(fiber)) { // 3. 判断是否需调度
scheduleUpdateOnFiber(fiber); // 4. 发起调度请求
}
}
-
核心作用:处理单个状态更新请求
-
批量相关逻辑:
- 条件跳过:当 fiber 已在更新中时阻止重复调度
- 队列合并:将连续更新收集到同一队列(如多次 setState)
2. ensureRootIsScheduled
层级:更新消费层
scss
function ensureRootIsScheduled(root) {
// 1. 检查现有任务
if (existingTask !== null) {
// 2. 决策:复用/替换/跳过任务
if (newPriority === existingPriority) {
return; // 已有相同任务 → 跳过
}
cancelTask(existingTask); // 取消低优先级任务
}
// 3. 创建新任务(批量处理所有待处理更新)
scheduleCallback(priority, performSyncWorkOnRoot);
}
-
核心作用:协调整个应用级别的更新执行
-
批量相关逻辑:
- 任务去重:合并同一优先级的多个调度请求
- 优先级仲裁:决定哪些更新需要立即处理/批量延迟
- 时间切片:将多个 root 更新合并到单个渲染周期
场景模拟:多组件同时更新
scss
// 组件A
const [a, setA] = useState(0);
setA(1); // 触发dispatchAction
// 组件B (不同fiber)
const [b, setB] = useState(0);
setB(1); // 触发dispatchAction
处理流程:
-
dispatchAction 层
- 组件A:创建update → 加入队列 → 发起调度请求
- 组件B:创建update → 加入队列 → 发起调度请求
-
ensureRootIsScheduled 层
- 关键决策:将两个独立请求合并为单个渲染任务
问题 | dispatchAction解决 | ensureRootIsScheduled解决 |
---|---|---|
单组件多次更新 | 队列合并(防止多次调度) | 不涉及(已在fiber层合并) |
多组件更新冲突 | 无法解决 | 跨fiber任务仲裁 |
优先级抢占 | 无处理能力 | 取消低优任务,执行高优任务 |
时间切片利用 | 无法优化 | 合并多个root更新到单次渲染循环 |
渲染帧率稳定 | 无控制 | 通过任务分片保障60fps |
总结:为什么需要双层设计?
-
关注点分离
- 微观:
dispatchAction
处理单个更新原子操作 - 宏观:
ensureRootIsScheduled
协调系统级更新策略
- 微观: