useState
概述
useState 是 React Hooks 体系里最常用的状态管理 API。表面看就是"声明一个状态变量 + 拿到一个 setter",但底层涉及Fiber 架构、Hook 链表、Dispatcher 状态机、Update 循环链表四套机制。
本篇按"存什么 → 怎么存 → 怎么更新"的顺序拆解,重点回答:
- state 数据存在哪?
- mount 和 update 两个阶段的代码路径有何不同?
- 连续
setState、值没变、闭包陷阱... 这些边界情况源码是怎么处理的?
一、读源码前的四个前置概念
| 概念 | 作用 | 所在位置 |
|---|---|---|
| Fiber | React 16+ 的虚拟 DOM 节点,每个函数组件对应一个 Fiber | 引擎全局 |
| Hook 链表 | 单个 Fiber 上所有 hooks 串联成的单链表 | fiber.memoizedState |
| Dispatcher | 状态机对象,根据 mount/update 切换不同实现 | ReactCurrentDispatcher.current |
| Update 队列 | 一次 setState 对应一个 Update 节点,多个 Update 串成循环链表 | hook.queue.pending |
关键认识:
useState本身只是个"状态机中转站"——它根据当前 Dispatcher 指向,调用mountState或updateState。真正存 state 的是 Hook 链表。理解这一点,整篇源码就能串起来。
二、数据结构
2.1 Hook 节点
挂在 fiber.memoizedState 上的单链表节点:
{
memoizedState: any, // 当前 state 值(最终给组件用的就是这个)
baseState: any, // update 处理前的基线值
baseQueue: Update | null, // 还未处理的 update 队列
queue: UpdateQueue | null, // 包含 pending 队列 + dispatch 函数
next: Hook | null, // 指向下一个 hook
}
2.2 Update 节点
每次 setState 会产生一个 Update 节点,组成循环链表:
{
lane: number, // 优先级(React 18 并发模式相关)
action: any, // 新值(或返回新值的函数)
next: Update | null, // 指向下一个 update
eagerReducer: Reducer, // 上一次用的 reducer(用于 bail-out 优化)
eagerState: any, // 上一次的旧值
}
循环链表的妙处:O(1) 追加新节点。新 update 总是插在
queue.pending后面,老节点无需遍历。完整原理在 dispatchAction 部分展开。
三、renderWithHooks —— 所有 hooks 的统一入口
对于函数组件,React 在 beginWork 阶段会调用 renderWithHooks 来注册当前渲染上下文。根据 mount/update 切换不同的 Dispatcher:
// renderWithHooks
function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderLanes,
) {
// 当前 fiber
currentlyRenderingFiber$1 = workInProgress;
// hooks 队列
workInProgress.memoizedState = null;
// 副作用队列
workInProgress.updateQueue = null;
{
if (current !== null && current.memoizedState !== null) {
// 有老 hook 链 → update 阶段
ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
} else {
// 首次挂载 → mount 阶段
ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
}
}
// ...执行组件函数...
return children;
}
关键点:useState 在源码里实现极简,只是从 Dispatcher 取个方法:
// useState 本体
function useState(initialState) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
真正的逻辑都在 Dispatcher 指向的 mountState / updateState 里。
mount 流程图:

update 流程图:

四、mount 阶段
4.1 Dispatcher 的 useState 实现
useState: function (initialState) {
// ...前置检查
try {
return mountState(initialState);
} finally {
// 恢复 Dispatcher,防止渲染中嵌套调用其他 hooks 出错
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
4.2 创建 Hook 节点 —— mountWorkInProgressHook
function mountWorkInProgressHook() {
// 创建一个空的 hook 节点
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
// 如果是第一个 hook,挂到 fiber.memoizedState 上
// 否则接到链表尾部(next 指针)
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
这就是 hooks 顺序必须稳定 的原因:mount 阶段按调用顺序建链表,如果中间有条件分支,链表顺序就乱了,update 阶段会拿错 hook。
4.3 初始化 state —— mountState
function mountState(initialState) {
// 1️⃣ 创建一个 hook 节点
var hook = mountWorkInProgressHook();
// 2️⃣ 如果 initialState 是函数,则调用它(支持惰性初始化)
if (typeof initialState === "function") {
initialState = initialState();
}
// 3️⃣ 初始化 state
hook.memoizedState = hook.baseState = initialState;
// 4️⃣ 创建 update queue
var queue = (hook.queue = {
pending: null, // update 循环链表
dispatch: null, // setter 函数
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
});
// 5️⃣ 绑定 dispatchAction(闭包,预先绑定 fiber 和 queue)
var dispatch = (queue.dispatch = dispatchAction.bind(
null,
currentlyRenderingFiber$1,
queue,
));
return [hook.memoizedState, dispatch];
}
// useState 默认的 reducer
function basicStateReducer(state, action) {
return typeof action === "function" ? action(state) : action;
}
设计点:
dispatchAction通过bind预绑定了fiber和queue,这样每次 setState 都不需要再传这两个参数,省下大量闭包开销——这也是为什么 setter 引用可以稳定(不会引起子组件 re-render)。
五、update 阶段
5.1 流程概览
- 调用
setState(newValue)→ dispatchAction 收集 update - dispatchAction 调度一次 React 更新
- 下次
renderWithHooks触发,Dispatcher 切到 OnUpdate - 组件函数重新执行,
useState被再次调用,命中updateState→updateReducer updateReducer遍历 update 队列,reducer 逐个执行,得到最新 state- 渲染真实 DOM
5.2 核心:dispatchAction
function dispatchAction(fiber, queue, action) {
// 1️⃣ 创建 update 节点
var update = {
lane: lane, // 优先级
action: action, // 新值
next: null, // 循环链表指针
eagerReducer: null, // bail-out 优化用
eagerState: null,
};
// 2️⃣ 把 update 插入循环链表(O(1) 操作)
var pending = queue.pending;
if (pending === null) {
// 第一个 update:自己指向自己形成环
update.next = update;
} else {
// 后续 update:插到 pending 之后
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
// 3️⃣ Bail-out 优化(值没变就不调度更新)
var alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber$1 ||
(alternate !== null && alternate === currentlyRenderingFiber$1)
) {
// 渲染期间 setState,标记一下
didScheduleRenderPhaseUpdateDuringThisPass = true;
} else {
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
var lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
// 尝试在 dispatch 阶段直接算出新 state
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
// ⚡ 关键 bail-out:新旧值一致(Object.is)就不调度更新
if (objectIs(eagerState, currentState)) {
return; // 直接返回,不触发 render
}
}
}
// 4️⃣ 发起一次调度更新
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}
bail-out 是性能优化的关键:
setState(同一个值)不会触发 render,因为Object.is(new, old)为 true 时直接return。这也是为什么setCount(count + 1)看起来没变化但实际是触发了更新的(Object.is比较的是值)。
5.3 复制旧 Hook 节点 —— updateWorkInProgressHook
function updateWorkInProgressHook() {
// 1️⃣ 找到 current fiber 上对应的老 hook
var nextCurrentHook;
if (currentHook === null) {
var current = currentlyRenderingFiber$1.alternate;
nextCurrentHook = current !== null ? current.memoizedState : null;
} else {
nextCurrentHook = currentHook.next;
}
// 2️⃣ 找到 workInProgress fiber 上对应的位置
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
// 3️⃣ 复用或新建 hook 节点
if (nextWorkInProgressHook !== null) {
// 复用老节点
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 节点数不匹配,需要新建
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
设计点:update 阶段复用了老 hook 节点(不是重建),只更新
memoizedState。这就是为什么 hooks 调用顺序必须稳定——节点位置错位会导致currentHook取错值。
5.4 计算新 state —— updateReducer
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue;
// ...省略 baseQueue 处理
// 遍历 update 循环链表,依次执行 reducer
do {
// ...
if (update.eagerReducer === reducer) {
// bail-out 命中,直接用上次的值
newState = update.eagerState;
} else {
// 真正的 reducer 调用
var action = update.action;
newState = reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
// 把最终 state 写回 hook 节点
hook.memoizedState = newState;
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
5.5 简单总结 update 过程
- 调用 setter → 触发
dispatchAction - 收集 update,按序插入 update 循环链表
queue.pending - bail-out 检查:新值 == 旧值则直接 return,不调度更新
- 调度一次 React 更新(
scheduleUpdateOnFiber) - 重新执行组件函数 →
useState再次调用 → 命中updateState updateReducer遍历 update 队列,依次执行 reducer,拿到最新 state- 渲染真实 DOM,结束
六、面试高频 Q&A
Q1: 为什么 React 16 之前函数式组件不能拥有状态?
16 之前只有类组件在更新时存在实例(
this上挂数据)。16 之后 Fiber 架构出现,每个函数组件都对应一个 Fiber 实例,就有了保存状态的能力。
Q2: 为什么只能在函数组件中使用 hooks?
只有函数组件走
renderWithHooks的逻辑。类组件有自己的实例机制,不依赖 Dispatcher 状态机。
Q3: 为什么 hooks 必须在函数最外层调用(不能在循环/条件里)?
因为 update 阶段靠 位置对应 复用老 hook 节点(
currentHook.next顺序遍历)。如果 mount 阶段有条件分支、update 阶段没有,链表就错位了,取到的 state 就会错乱。
Q4: setState 是同步还是异步?
默认是异步(批处理)。源码里
dispatchAction走scheduleUpdateOnFiber调度,不会立刻执行组件函数。直到 commit 阶段完成后才一次性 re-render。但不是绝对异步:
- 事件回调内 → 批处理(异步)
setTimeout/ 原生事件回调 → 同步更新- React 18
createRoot后 → 全部批处理(包括setTimeout/Promise)
Q5: 连续多次 setState 会触发多次 render 吗?
onClick = () => {
setState(0);
setState(1);
setState(2);
};
不会。事件回调内处于批量更新状态,三次 setState 只是把 3 个 update 追加到
queue.pending循环链表。批量更新结束后才 re-render,组件函数只重新执行一次,3 个 reducer 顺序执行,最终 state = 2。细节:如果
setState(0)、setState(0)两次都设了相同的值,第二次的 bail-out 会直接 return,update 都不进队列。
Q6: 执行两次 setState(0) 会执行两次函数组件吗?
不会。
dispatchAction里的 bail-out 检查会用ObjectIs比较新旧值,相同就return,不调度更新。重要区别:
setCount(0)→setCount(0):值没变,bail-out,只跑一次组件函数setCount(0)→setCount(1):值变了,调度一次更新,只跑一次组件函数(不是两次)setCount(c => c + 1)→setCount(c => c + 1):依赖前值,一定变,跑一次组件函数得到累加结果
Q7: 为什么 setter 引用是稳定的?
源码里
dispatch是dispatchAction.bind(null, fiber, queue)的闭包结果。同一个 hook 的dispatch永远是同一个引用。这就是为什么
useEffect里setCount不需要加到依赖数组——它的引用不会变。