React Hook
社区来稿 · Liu由用户发布,不代表 batype 本人观点
·1063 字 · 约 3 分钟
React Hook 的原理实现。
React Hook 原理与实现总结#
一、React Hook 原理简述#
- 本质:
React Hook 是通过“链表”结构,将每个 Hook 的状态挂载在当前函数组件对应的 Fiber 节点(FiberNode)的 memoizedState 属性上。 - 顺序保证:
每次渲染时,React 通过链表顺序遍历 Hook,确保每个 Hook 状态与调用顺序一一对应。 - 更新机制:
每次 setState/useReducer 等触发更新时,都会将更新 action 以 update 节点的形式加入 queue 队列,等到下次渲染时统一处理。
二、Hook 节点结构与字段含义#
以 useState/useReducer 为例,每个 Hook 节点结构大致如下:
const hook = {
memoizedState, // 当前 Hook 的状态值
baseState, // 上一次的状态(用于优化和回溯)
queue, // 更新队列(存放 setState 产生的 update)
next, // 指向下一个 Hook 节点
}
字段详细说明#
| 字段名 | 含义 | 可能存在的问题/注意点 |
|---|---|---|
| memoizedState | 当前 Hook 的状态值(如 useState 的 state,useRef 的 current) | 1. 状态类型必须与初始值一致 2. 顺序不能变 |
| baseState | 上一次的状态(用于优化、回溯、跳过部分 update) | 1. 仅在并发模式/多次更新时有用 2. 可能过期 |
| queue | 更新队列,存放所有待处理的 update(环状链表) | 1. 队列过长可能影响性能 2. 批量更新需谨慎 |
| next | 指向下一个 Hook 节点,形成链表 | 1. Hook 调用顺序不能变 2. 条件 Hook 会出错 |
queue 字段内部结构#
const queue = {
pending, // 指向环状链表的最后一个 update 节点
dispatch, // setState 函数
// useReducer 时还会有 lastRenderedReducer, lastRenderedState
}
update 节点结构#
const update = {
action, // setState 传入的值或函数
next, // 指向下一个 update,形成环状链表
}
三、常见问题与注意事项#
- Hook 调用顺序不能变
- 不能在条件、循环、嵌套函数中调用 Hook,否则链表顺序错乱,状态错位。
- 状态类型要一致
- memoizedState 类型要和初始值一致,否则可能出现类型错误。
- 批量更新与性能
- queue 队列过长时,批量处理所有 update 可能带来性能问题。
- 并发模式下的状态一致性
- baseState、queue 设计是为了解决并发渲染下的状态一致性问题,但极端情况下可能出现“回滚”或“丢失更新”。
- useRef 特例
- useRef 只用 memoizedState 存储 current,不涉及 queue。
四、Hook 链表结构图#
五、简要流程#
- 首次渲染:依次创建 Hook 节点,挂到 FiberNode.memoizedState 链表上。
- 更新时:遍历链表,依次处理每个 Hook 的 queue 队列,更新 memoizedState。
- setState:创建 update 节点,插入 queue.pending 环状链表,下次渲染时统一处理。
六、参考代码片段#
// Hook 节点
const hook = {
memoizedState: 0, // 当前 state
baseState: 0, // 上一次 state
queue: {
pending: null, // update 环状链表
dispatch: null, // setState
},
next: null, // 下一个 Hook
}
为什么 queue.pending 采用环状链表,以及为什么更新时需要遍历整个链表,而不是只处理最后一项。#
1. queue.pending 是什么?#
- queue.pending 是一个环状单向链表(circular linked list),每个节点代表一次 setState 产生的更新(update)。
- 每次 setState,都会把新的 update 节点插入到链表中,并让 queue.pending 指向最新的那个节点。
2. 为什么不是只处理最后一项?#
1)React 支持多次 setState 合并批量处理#
- 在一次事件循环中,可能会多次调用 setState(比如在同一个事件处理函数里多次 setState)。
- React 需要保证所有的更新都被依次处理,而不是只处理最后一次。
- 比如:
setState(x => x + 1);
setState(x => x + 1);
setState(x => x + 1);
你期望 state 最终加 3,而不是只加 1。
2)每个 update 可能是函数式更新,必须依次应用#
- 如果 update 是函数(如 setState(x => x + 1)),每个更新都依赖上一次的结果,不能跳过。
- 只有依次遍历链表,把每个 update 应用到 state 上,才能得到正确的最终结果。
3. 为什么用环状链表?#
- 环状链表方便在 O(1) 时间内插入新节点(只需操作 queue.pending 指针)。
- 处理时,从 queue.pending.next 开始遍历,直到回到 queue.pending,能保证所有更新都被处理且顺序正确。
4. 伪代码说明#
// 插入新 update
function enqueueUpdate(queue, update) {
if (queue.pending === null) {
update.next = update; // 自己指向自己
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update; // 指向最新的 update
}
// 处理所有 update
function processQueue(queue, baseState) {
let newState = baseState;
let first = queue.pending?.next;
let update = first;
do {
newState = typeof update.action === 'function'
? update.action(newState)
: update.action;
update = update.next;
} while (update !== first);
queue.pending = null;
return newState;
}
5. 总结#
- 不能只处理最后一项,因为每个 setState 都可能影响最终结果,必须全部依次处理。
- 环状链表方便插入和遍历,保证所有更新都能被顺序处理。
- 这样才能实现 React 的批量更新、函数式 setState 等特性,保证状态一致性。
如果你还想了解更底层的源码细节或有其他相关问题,欢迎继续提问!