React useState 深度源码原理解析
前言
useState 是 React Hooks 中最基础、最常用的 Hook,它让函数组件拥有了管理状态的能力。但你是否想过:
- 函数组件每次渲染都会重新执行,
useState是如何"记住"上一次的状态的? - 为什么 Hooks 不能写在条件语句或循环中?
setState之后到底发生了什么?- React 内部是如何区分"首次挂载"和"后续更新"的?
本文将从源码层面,逐行拆解 useState 的完整实现链路,彻底搞懂它的底层原理。
一、从一个简单的例子开始
jsx
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击了 {count} 次
</button>
);
}
每次点击按钮,count 加 1,组件重新渲染。表面上看非常简单,但背后涉及 React 的 Fiber 架构 、链表结构 、闭包机制 和 调度系统。
二、核心数据结构:Hook 链表
2.1 Fiber 节点与 Hook 的关系
React 内部为每个组件维护了一个 Fiber 节点 (可以理解为组件的"身份证")。函数组件的所有 Hook 状态,都挂在 Fiber 节点的 memoizedState 属性上:
yaml
Fiber {
memoizedState: Hook(链表头节点),
stateNode: ...,
...
}
2.2 Hook 对象的结构
每次调用 useState、useEffect 等 Hook,React 都会创建一个 Hook 对象 ,多个 Hook 通过 next 指针串成链表:
typescript
export type Hook = {
memoizedState: any, // 存储当前 Hook 的状态值
baseState: any, // 基础状态(用于更新合并计算)
baseQueue: Update<any, any> | null, // 基础更新队列
queue: UpdateQueue<any, any> | null, // 当前 Hook 的更新队列
next: Hook | null, // 指向下一个 Hook 节点 → 形成链表
};
2.3 链表可视化
假设组件中有三个 useState:
jsx
function App() {
const [name, setName] = useState('React');
const [age, setAge] = useState(18);
const [visible, setVisible] = useState(true);
// ...
}
React 内部会形成如下链表结构:
yaml
Fiber.memoizedState
↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Hook #1 │ │ Hook #2 │ │ Hook #3 │
│ memoizedState│ │ memoizedState│ │ memoizedState│
│ = 'React' │────▶│ = 18 │────▶│ = true │
│ queue: {...} │ │ queue: {...} │ │ queue: {...} │
│ next ─────────┤ │ next ─────────┤ │ next: null │
└──────────────┘ └──────────────┘ └──────────────┘
🔑 关键设计:React 选择链表而非数组来存储 Hook,原因有三:
- 动态扩展:组件中 Hook 数量不固定,链表无需预分配空间
- 顺序访问高效:配合指针按调用顺序逐个访问,时间复杂度 O(1)
- 组件隔离:每个组件的 Fiber 拥有独立的 Hook 链表,互不干扰
三、两套 Dispatcher:Mount vs Update
3.1 React 如何区分首次渲染和更新?
这是 useState 实现中最精妙的设计之一。React 维护了 两套完全不同的 Hook 实现 ,通过一个全局变量 ReactCurrentDispatcher 来切换:
javascript
// react 模块中,useState 的入口极其简洁
export function useState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
这里的 dispatcher 会根据当前渲染阶段,指向不同的实现:
javascript
// 判断逻辑在 renderWithHooks 中
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount // 首次挂载
: HooksDispatcherOnUpdate; // 后续更新
3.2 两套 Dispatcher 的完整定义
Mount 阶段的 Dispatcher:
javascript
const HooksDispatcherOnMount: Dispatcher = {
useState: mountState,
useEffect: mountEffect,
useRef: mountRef,
useMemo: mountMemo,
useCallback: mountCallback,
useReducer: mountReducer,
useContext: readContext,
// ... 其他 Hook
};
Update 阶段的 Dispatcher:
javascript
const HooksDispatcherOnUpdate: Dispatcher = {
useState: updateState,
useEffect: updateEffect,
useRef: updateRef,
useMemo: updateMemo,
useCallback: updateCallback,
useReducer: updateReducer,
useContext: readContext,
// ... 其他 Hook
};
💡 设计意图:mount 时需要初始化 Hook 节点、创建链表;update 时只需按顺序复用已有节点、处理更新队列。将两套逻辑彻底分离,代码更清晰,性能也更优。
四、Mount 阶段:mountState 完整流程
当组件首次渲染时,useState 实际调用的是 mountState。
4.1 mountState 源码
javascript
function mountState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
// 第一步:创建 Hook 对象并挂到链表上
const hook = mountWorkInProgressHook();
// 第二步:处理初始值(支持函数式初始化)
if (typeof initialState === 'function') {
initialState = initialState();
}
// 第三步:赋值初始状态
hook.memoizedState = hook.baseState = initialState;
// 第四步:创建更新队列
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null, // 待处理的更新(环形链表)
interleaved: null, // 交错更新
lanes: NoLanes, // 优先级
dispatch: null, // setState 函数
lastRenderedReducer: basicStateReducer, // 上一次的 reducer
lastRenderedState: (initialState: any), // 上一次渲染的 state
};
hook.queue = queue;
// 第五步:创建 dispatch 函数(即 setState)
const dispatch: Dispatch<BasicStateAction<S>> =
(queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber, // 绑定当前 Fiber
queue, // 绑定更新队列
));
// 第六步:返回 [state, setState]
return [hook.memoizedState, dispatch];
}
4.2 mountWorkInProgressHook 详解
这个函数是 几乎所有 Hook 在 mount 阶段都会调用 的核心函数,负责创建 Hook 节点并串联链表:
javascript
function mountWorkInProgressHook(): Hook {
// 创建一个全新的 Hook 对象
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 这是组件的第一个 Hook
// 将其设为 Fiber 的 memoizedState(链表头)
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 不是第一个 Hook,追加到链表末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
执行过程图解(以两个 useState 为例):
ini
第一次调用 useState('React'):
workInProgressHook = null
→ 创建 Hook#1
→ Fiber.memoizedState = Hook#1
→ workInProgressHook = Hook#1
第二次调用 useState(18):
workInProgressHook = Hook#1
→ 创建 Hook#2
→ Hook#1.next = Hook#2
→ workInProgressHook = Hook#2
最终链表:
Fiber.memoizedState → Hook#1 → Hook#2 → null
4.3 关于函数式初始化
注意 mountState 中的这段逻辑:
javascript
if (typeof initialState === 'function') {
initialState = initialState();
}
这就是为什么 useState 支持这样的写法:
javascript
// 惰性初始化:函数只在 mount 时执行一次
const [data, setData] = useState(() => {
return expensiveComputation(props);
});
当初始值需要昂贵计算时(如从 localStorage 读取、解析 URL 参数),使用函数式初始化可以避免每次渲染都重复计算。
五、Update 阶段:updateState 完整流程
当调用 setState 触发重新渲染后,组件函数重新执行,此时 useState 调用的是 updateState。
5.1 updateState 的秘密
javascript
function updateState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
惊喜! useState 在更新阶段其实就是 useReducer!它使用了一个内置的 basicStateReducer:
javascript
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
这个 reducer 的逻辑非常简单:
- 如果
action是函数(函数式更新),则执行action(state)得到新状态 - 如果
action是普通值(直接赋值),则直接用action作为新状态
这也解释了为什么:
javascript
// 直接赋值 → action = 5 → 返回 5
setCount(5);
// 函数式更新 → action = (prev) => prev + 1 → 返回 prev + 1
setCount(prev => prev + 1);
5.2 updateReducer 核心流程(简化)
javascript
function updateReducer<S, A>(
reducer: (S, A) => S,
initialArg: S,
): [S, Dispatch<A>] {
// 第一步:获取当前 Hook 节点(按顺序从链表中取)
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 第二步:取出更新队列
const pending = queue.pending;
queue.pending = null;
// 第三步:遍历更新队列,依次计算新 state
if (pending !== null) {
let first = pending.next; // 环形链表的第一个节点
let newState = hook.baseState;
let update = first;
do {
// 应用每个 update
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== first); // 遍历完环形链表
hook.memoizedState = newState;
hook.baseState = newState;
}
// 第四步:返回最新的 state 和 dispatch
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
5.3 updateWorkInProgressHook
更新阶段不再创建新的 Hook 节点,而是 按顺序复用 已有的节点:
javascript
function updateWorkInProgressHook(): Hook {
// 从 current Fiber 的 Hook 链表中,按顺序取下一个
let nextCurrentHook;
if (currentHook === null) {
// 第一个 Hook
const current = currentlyRenderingFiber.alternate;
nextCurrentHook = current.memoizedState;
} else {
// 后续 Hook
nextCurrentHook = currentHook.next;
}
currentHook = nextCurrentHook;
// 基于 current Hook 创建新的 workInProgress Hook
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
// 同样串联成链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
return workInProgressHook;
}
⚠️ 这就是 Hook 不能写在条件语句中的根本原因! 更新阶段按
next指针顺序取 Hook 节点,如果某个 Hook 因条件判断被跳过,后续所有 Hook 都会取到错误的节点,导致状态错乱。
六、setState(dispatchSetState)的完整实现
当我们调用 setCount(count + 1) 时,实际执行的是 dispatchSetState。
6.1 源码分析
javascript
function dispatchSetState<S, A>(
fiber: Fiber, // 通过 bind 绑定的当前组件 Fiber
queue: UpdateQueue<S, A>, // 通过 bind 绑定的更新队列
action: A, // 用户传入的新值或更新函数
): void {
// 第一步:获取更新优先级
const lane = requestUpdateLane(fiber);
// 第二步:创建 update 对象
const update: Update<S, A> = {
lane,
action, // 用户传入的值,如 count + 1 或 prev => prev + 1
hasEagerState: false,
eagerState: null,
next: null,
};
// 第三步:判断是否在渲染过程中调用(极少数情况)
if (isRenderPhaseUpdate(fiber)) {
// 渲染阶段的更新,加入 queue.pending
enqueueRenderPhaseUpdate(queue, update);
} else {
// 第四步:将 update 加入环形链表
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// ⚡ 性能优化:Eager State
// 如果当前没有待处理的更新,可以提前计算新 state
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
const currentState = queue.lastRenderedState;
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 如果新旧 state 相同,直接跳过!不触发重新渲染
if (Object.is(eagerState, currentState)) {
// 🎉 Bailout! 省去一次不必要的渲染
return;
}
}
}
// 第五步:将 update 入队
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
// 第六步:调度更新
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
}
}
}
6.2 更新队列的环形链表结构
多次 setState 产生的 update 会形成环形链表:
javascript
// 假设连续调用三次 setState
setCount(1); // 创建 update1
setCount(2); // 创建 update2
setCount(3); // 创建 update3
// 环形链表结构(queue.pending 指向最后一个):
//
// queue.pending = update3
// ↓
// update3 → update1 → update2 → update3(回到起点)
//
// 特点:pending.next 就是第一个 update
为什么要用环形链表?因为 queue.pending 始终指向 最后一个 update,而 pending.next 就是 第一个 update,这样用 O(1) 的时间复杂度就能找到队头和队尾,非常高效。
6.3 Eager State 优化
dispatchSetState 中有一个重要的性能优化------Eager State(急切计算):
javascript
// 核心思路:如果当前没有排队的更新,就提前算出新 state
// 如果新旧 state 相同(通过 Object.is 比较),直接跳过渲染
if (Object.is(eagerState, currentState)) {
return; // 不触发 scheduleUpdateOnFiber,省去整个渲染流程
}
这就是为什么:
javascript
const [count, setCount] = useState(0);
// 连续点击以下按钮,第二次点击不会触发重新渲染
<button onClick={() => setCount(0)}>设为0</button>
七、renderWithHooks:串联一切的入口
在 Reconciler 的 beginWork 阶段处理函数组件时,会调用 renderWithHooks,这是整个 Hook 系统的入口:
javascript
function renderWithHooks(
current: Fiber | null, // 上一次的 Fiber(null 表示首次渲染)
workInProgress: Fiber, // 当前正在构建的 Fiber
Component: Function, // 函数组件本身
props: any, // props
secondArg: any, // context
nextRenderLanes: Lanes, // 渲染优先级
): any {
// 1️⃣ 设置全局变量
currentlyRenderingFiber = workInProgress;
// 2️⃣ 重置 Fiber 上的 Hook 相关属性
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
// 3️⃣ 切换 Dispatcher(核心!)
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount // mount:所有 Hook 走初始化逻辑
: HooksDispatcherOnUpdate; // update:所有 Hook 走更新逻辑
// 4️⃣ 执行函数组件!
// 此时组件内的 useState/useEffect 等会被依次调用
let children = Component(props, secondArg);
// 5️⃣ 重置全局变量
currentlyRenderingFiber = null;
currentHook = null;
workInProgressHook = null;
return children;
}
执行流程图:
scss
renderWithHooks 被调用
│
├──→ 判断 mount / update → 切换 Dispatcher
│
├──→ 执行 Component(props)
│ │
│ ├──→ 第1个 useState → mountState / updateState
│ ├──→ 第2个 useState → mountState / updateState
│ ├──→ useEffect → mountEffect / updateEffect
│ └──→ 返回 JSX
│
└──→ 清理全局变量,返回 children
八、完整流程图:从 setState 到页面更新
scss
用户调用 setCount(count + 1)
│
▼
dispatchSetState(fiber, queue, action)
│
├── 创建 update 对象
├── Eager State 优化(如果新旧相同则 bailout)
├── update 加入环形链表 queue.pending
└── scheduleUpdateOnFiber → 调度更新
│
▼
beginWork 阶段
│
▼
renderWithHooks(current, workInProgress, Component, props)
│
┌────────┴────────┐
│ 判断 mount/update │
│ 切换 Dispatcher │
└────────┬────────┘
│
▼
执行 Component(props)
│
┌────────┴────────┐
│ 遇到 useState │
│ → updateState │
│ → updateReducer │
│ → 遍历 queue.pending │
│ → 计算新 state │
│ → 返回 [newState, dispatch] │
└────────┬────────┘
│
▼
返回新的 JSX(children)
│
▼
completeWork 阶段
│
▼
commitWork 阶段
│
▼
DOM 更新,页面刷新 ✅
九、手写一个简化版 useState
理解了原理之后,我们来手写一个简化版,加深理解:
javascript
let hookStates = []; // 存储所有 hook 的状态
let hookIndex = 0; // 当前 hook 的索引
function useState(initialState) {
// 如果是首次渲染,使用初始值;否则使用已保存的状态
const currentIndex = hookIndex;
hookStates[currentIndex] = hookStates[currentIndex] !== undefined
? hookStates[currentIndex]
: (typeof initialState === 'function' ? initialState() : initialState);
// setState 通过闭包捕获 currentIndex
function setState(newState) {
// 支持函数式更新
const nextState = typeof newState === 'function'
? newState(hookStates[currentIndex])
: newState;
// 如果值没变,不触发更新(简化的 bailout)
if (Object.is(nextState, hookStates[currentIndex])) {
return;
}
hookStates[currentIndex] = nextState;
render(); // 触发重新渲染
}
hookIndex++; // 移动到下一个 hook 位置
return [hookStates[currentIndex], setState];
}
// 模拟渲染
function render() {
hookIndex = 0; // ⚠️ 每次渲染前重置索引!
ReactDOM.render(<App />, document.getElementById('root'));
}
注意:这个简化版使用数组+索引来模拟,React 真实实现使用的是链表。但核心思想是一致的------按调用顺序存取状态。
十、关键问题解答
Q1:为什么 Hook 不能写在条件语句中?
javascript
// ❌ 错误写法
function App() {
if (someCondition) {
const [a, setA] = useState(1); // Hook #1(可能不执行)
}
const [b, setB] = useState(2); // Hook #2
}
因为 React 在更新时通过 next 指针(链表顺序)来匹配 Hook 节点。如果 mount 时创建了 [Hook#1, Hook#2],但 update 时 someCondition 为 false,第一个执行的 useState 就会拿到 Hook#1 的状态(本应是 Hook#2 的),导致状态完全错乱。
Q2:useState 和 useReducer 是什么关系?
从源码可以看到,useState 在更新阶段直接调用了 updateReducer:
javascript
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
useState 本质上就是一个内置了 basicStateReducer 的 useReducer。
Q3:连续多次 setState 会渲染多次吗?
javascript
function handleClick() {
setCount(1);
setName('React');
setVisible(false);
}
不会。React 的事件处理中有 批量更新(batching) 机制。这三个 setState 产生的 update 会被放入各自的队列中,React 只会触发一次调度和渲染,在渲染时一次性处理所有 update。
Q4:函数式更新和直接赋值有什么区别?
javascript
// 直接赋值:基于闭包捕获的旧值
setCount(count + 1);
setCount(count + 1);
// 结果:count 只加了 1(两次都基于同一个旧值)
// 函数式更新:基于最新的 state
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 结果:count 加了 2(每次都基于上一次计算的结果)
从源码角度看,basicStateReducer 中:
- 直接赋值:
action就是值本身,直接替换 - 函数式更新:
action(state)会以当前最新 state 为参数计算
十一、总结
useState 的核心机制一览
| 机制 | 说明 |
|---|---|
| Fiber.memoizedState | 存储 Hook 链表头节点,是状态持久化的载体 |
| Hook 链表 | 每个 Hook 调用对应一个节点,通过 next 串联 |
| 两套 Dispatcher | mount 用 mountState 初始化,update 用 updateState 复用 |
| 环形更新队列 | 多次 setState 的 update 形成环形链表,统一在渲染时处理 |
| Eager State | 无待处理更新时提前计算,新旧相同则跳过渲染 |
| basicStateReducer | useState 本质是 useReducer 的语法糖 |
| 闭包绑定 | dispatch 通过 bind 绑定了 Fiber 和 queue,确保更新指向正确 |
设计哲学
- 用链表保证顺序:这是 "Hook 不能写在条件语句中" 这条规则的根本原因
- 用两套实现分离关注点:mount 专注初始化,update 专注状态计算
- 用闭包桥接函数组件和 Fiber 架构:让无状态的函数拥有了持久化状态的能力
- 用 Eager State 做性能优化:在调度之前就短路不必要的渲染
理解了这些底层原理,你不仅能更自信地使用 useState,更能在遇到诡异的状态问题时快速定位原因。
如果觉得本文有帮助,欢迎点赞收藏,也欢迎在评论区交流讨论!