React useState 深度源码原理解析

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 对象的结构

每次调用 useStateuseEffect 等 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,原因有三:

  1. 动态扩展:组件中 Hook 数量不固定,链表无需预分配空间
  2. 顺序访问高效:配合指针按调用顺序逐个访问,时间复杂度 O(1)
  3. 组件隔离:每个组件的 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 本质上就是一个内置了 basicStateReduceruseReducer

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,确保更新指向正确

设计哲学

  1. 用链表保证顺序:这是 "Hook 不能写在条件语句中" 这条规则的根本原因
  2. 用两套实现分离关注点:mount 专注初始化,update 专注状态计算
  3. 用闭包桥接函数组件和 Fiber 架构:让无状态的函数拥有了持久化状态的能力
  4. 用 Eager State 做性能优化:在调度之前就短路不必要的渲染

理解了这些底层原理,你不仅能更自信地使用 useState,更能在遇到诡异的状态问题时快速定位原因。


如果觉得本文有帮助,欢迎点赞收藏,也欢迎在评论区交流讨论!

相关推荐
z止于至善2 小时前
服务器发送事件(SSE):前端实时通信的轻量解决方案
前端·web·服务器通信
前端小棒槌2 小时前
TypeScript 核心知识点
前端
Selicens2 小时前
turbo迁移vite+(vite-plus)实践
前端·javascript·vite
答案answer2 小时前
我的Three.js3D场景编辑器免费开源啦🎉🎉🎉
前端·github·three.js
欧阳天羲2 小时前
AI 时代前端工程师发展路线
前端·人工智能·状态模式
Moment2 小时前
从爆红到被嫌弃,MCP 为什么开始失宠了
前端·后端·面试
code202 小时前
microapp 通过链接区分主子应用步骤
前端
IT 行者2 小时前
Claude Code Viewer: 打造 Web 端 Claude Code 会话管理利器
前端·人工智能·python·django
张毫洁3 小时前
vue2项目搭建
前端·vue.js·node.js