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 指向,调用 mountStateupdateState真正存 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 预绑定了 fiberqueue,这样每次 setState 都不需要再传这两个参数,省下大量闭包开销——这也是为什么 setter 引用可以稳定(不会引起子组件 re-render)。


五、update 阶段

5.1 流程概览

  1. 调用 setState(newValue)dispatchAction 收集 update
  2. dispatchAction 调度一次 React 更新
  3. 下次 renderWithHooks 触发,Dispatcher 切到 OnUpdate
  4. 组件函数重新执行,useState 被再次调用,命中 updateStateupdateReducer
  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 是同步还是异步?

默认是异步(批处理)。源码里 dispatchActionscheduleUpdateOnFiber 调度,不会立刻执行组件函数。直到 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 引用是稳定的?

源码里 dispatchdispatchAction.bind(null, fiber, queue)闭包结果。同一个 hook 的 dispatch 永远是同一个引用。

这就是为什么 useEffectsetCount 不需要加到依赖数组——它的引用不会变。