Skip to main content

useState

概述​

useState 是 React Hooks 体系里最常用的状态管理 API。表面看就是"声明一个状态变量 + 拿到一个 setter",但底层涉及Fiber 架构、Hook 链表、Dispatcher 状态机、Update 循环链表四套机制。

本篇按"存什么 → 怎么存 → 怎么更新"的顺序拆解,重点回答:

  1. state 数据存在哪?
  2. mount 和 update 两个阶段的代码路径有何不同?
  3. 连续 setState、值没变、闭包陷阱... 这些边界情况源码是怎么处理的?

一、读源码前的四个前置概念​

概念作用所在位置
FiberReact 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 流程图:

usestate源码执行顺序mount.png

update 流程图:

usestate源码执行顺序.png


四、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 流程概览​

  1. 调用 setState(newValue) → dispatchAction 收集 update
  2. dispatchAction 调度一次 React 更新
  3. 下次 renderWithHooks 触发,Dispatcher 切到 OnUpdate
  4. 组件函数重新执行,useState 被再次调用,命中 updateState → updateReducer
  5. updateReducer 遍历 update 队列,reducer 逐个执行,得到最新 state
  6. 渲染真实 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 过程​

  1. 调用 setter → 触发 dispatchAction
  2. 收集 update,按序插入 update 循环链表 queue.pending
  3. bail-out 检查:新值 == 旧值则直接 return,不调度更新
  4. 调度一次 React 更新(scheduleUpdateOnFiber)
  5. 重新执行组件函数 → useState 再次调用 → 命中 updateState
  6. updateReducer 遍历 update 队列,依次执行 reducer,拿到最新 state
  7. 渲染真实 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 不需要加到依赖数组——它的引用不会变。