深入剖析 useState产生的 setState的完整执行流程

我们结合上一篇文章React 执行阶段与渲染机制详解(基于 React 18+ 官方文档)React 执行阶段与渲染机制详解(基于 Rea - 掘金梳理的 React 执行阶段(Render Phase, Commit Phase, Passive Effect Phase)和状态管理机制,来深入剖析 useState 产生的 setState 的完整执行流程。

理解 setState 的行为是掌握 React 状态管理的核心。它远非简单的"设置一个值",而是一个触发复杂协调和渲染流程的起点。


🧩 useStatesetState 的核心概念

首先,明确几个关键点:

  1. useState 是 Hook :在函数组件首次渲染时调用,返回一个状态变量和一个 setState 函数。
  2. setState 是调度器 (Scheduler) :调用 setState 不会立即更新状态,也不会立即触发重新渲染。它只是向 React 的调度系统"请求"一次状态更新。
  3. 状态更新是异步且可能批量的 :React 会为了性能,将多个 setState 调用合并成一次更新,尤其是在事件处理器中。
  4. 状态是不可变的快照 :每次渲染,组件函数都会从头开始执行,拿到的是当前渲染周期的 state 快照。setState 会安排下一次渲染使用新的状态。

🔄 setState 的详细执行流程

当您在组件中调用 setState(newValue) 时,以下流程被触发:

第 1 步:触发更新请求 (Trigger Update Request)

  • 发生时机 :在您的代码中调用 setScore(score + 1)setScore(prev => prev + 1)
  • 内部动作
    • React 将这个更新请求(包含新值或更新函数)放入一个更新队列 (Update Queue) 中,与当前 Fiber 节点(代表您的组件实例)关联。
    • React 标记该 Fiber 节点及其父路径上的节点为"需要更新"。
    • 关键点 :此时,组件函数不会 重新执行,DOM 不会 更新,state 变量的值在当前函数作用域内依然保持旧值

第 2 步:进入 Render Phase(渲染/协调阶段)

  • 发生时机:React 的调度器(Scheduler)在合适的时机(通常是当前浏览器任务结束后,或根据并发模式的优先级)开始处理更新队列。
  • 内部动作
    1. 计算新状态
      • React 会处理该组件 Fiber 节点上的所有待定更新。
      • 如果 setState 传递的是函数 (如 setScore(prev => prev + 1)),React 会按顺序调用这些函数,将上一个函数的返回值作为下一个函数的参数,最终得到一个新值。
      • 如果传递的是 (如 setScore(5)),则直接使用该值(如果多次调用,后面的会覆盖前面的,除非是函数形式)。
    2. 触发组件重新渲染
      • React 使用计算出的新状态重新执行您的整个函数组件。
      • 在这次新的执行中,useState Hook 会返回这个新的状态值
      • 组件根据新的 stateprops 生成新的 JSX(Virtual DOM)。
    3. Reconciliation (协调)
      • React 将新生成的 Virtual DOM 树与上一次渲染的树进行比较(Diff 算法)。
      • 计算出最小的、必要的 DOM 变更列表。
    4. 可中断性:在 Concurrent Mode 下,这个阶段是可以被更高优先级的任务(如用户输入)中断的。React 可能会丢弃当前的渲染结果,稍后再重新开始。

第 3 步:进入 Commit Phase(提交阶段)

  • 发生时机:Render Phase 成功完成,计算出最终的变更列表后。
  • 内部动作
    1. Mutation (变更) :React 根据 Diff 结果,同步地更新真实的 DOM。这是用户能看到界面变化的时刻。
    2. Layout Effects :执行所有 useLayoutEffect 的清理函数(来自上一次渲染),然后执行本次渲染的 useLayoutEffect 创建函数。这些是同步执行的,发生在浏览器绘制之前,可以用来读取或同步修改刚刚更新的 DOM。
    3. 浏览器绘制:浏览器根据更新后的 DOM 进行布局(Layout)和绘制(Paint),用户看到最终的视觉效果。

第 4 步:进入 Passive Effect Phase(被动副作用阶段)

  • 发生时机:在浏览器完成绘制之后,异步执行。
  • 内部动作
    • 执行所有 useEffect 的清理函数(来自上一次渲染)。
    • 执行本次渲染的 useEffect 创建函数。
    • 这是进行数据获取、设置订阅、记录日志等副作用操作的理想位置,因为它不会阻塞视觉更新。

🎯 关键行为与最佳实践

1. 状态更新的异步性与批量处理

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1); // 请求更新 1
    setCount(count + 1); // 请求更新 2 (基于旧的 count)
    // 最终 count 只会 +1,因为两次都基于同一个旧值
    // React 可能会将这两次更新合并,只触发一次重新渲染
  };

  const handleClickCorrect = () => {
    setCount(prev => prev + 1); // 请求更新 1 (基于上一次的状态)
    setCount(prev => prev + 1); // 请求更新 2 (基于更新 1 后的状态)
    // 最终 count 会 +2,因为每次更新都基于最新的状态
    // 即使合并,函数也会按顺序执行
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Bad Update</button>
      <button onClick={handleClickCorrect}>Good Update</button>
    </div>
  );
}
  • 推荐 :当新状态依赖于旧状态时,务必使用函数式更新 (setState(prev => newValue))。这能确保你总是基于最新的状态进行计算,避免因异步和批量更新导致的竞态条件。

2. Render Phase 的纯函数性

在组件函数执行期间(Render Phase),你拿到的 state 是一个固定的快照。任何在 Render Phase 中对 state 的"修改"尝试都是徒劳的,因为下一次渲染会带来新的快照。

jsx 复制代码
function MyComponent() {
  const [value, setValue] = useState(0);

  // ❌ 错误:在渲染过程中直接修改 state 是无意义的,并且违反了纯函数原则
  // setValue(value + 1); // 这会导致无限循环!

  // ✅ 正确:在事件处理器或 Effect 中更新 state
  const handleClick = () => {
    setValue(value + 1);
  };

  return <button onClick={handleClick}>Increment</button>;
}

3. 状态保留与组件身份

正如前面文章所述,React 通过组件在树中的位置 来决定是否保留状态。setState 更新的是当前组件实例的状态。

jsx 复制代码
function App() {
  const [show, setShow] = useState(true);

  return (
    <div>
      <Counter /> {/* 这个 Counter 的状态始终保留 */}
      {show && <Counter />} {/* 这个 Counter 在 show 为 false 时会被卸载,状态丢失;show 为 true 时重新挂载,状态重置 */}
      <button onClick={() => setShow(!show)}>
        Toggle Second Counter
      </button>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0); // 每次挂载都会初始化为 0
  return (
    <div>
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>Add one</button>
    </div>
  );
}

调用第二个 CountersetScore 只会影响它自己的状态。当它被卸载时,其状态会被销毁。重新挂载时,useState(0) 会再次执行,状态重置为初始值。


📊 总结:setState 生命周期图谱

scss 复制代码
[用户点击按钮]
        ↓
[调用 setState(newValue/UpdaterFn)] → (更新入队,标记 Fiber)
        ↓
[React Scheduler 触发 Render Phase]
        ├──> [计算新状态] (处理更新队列)
        ├──> [重新执行组件函数] (拿到新 state)
        ├──> [生成新 Virtual DOM]
        └──> [Reconciliation (Diff)]
        ↓
[Commit Phase]
        ├──> [Mutation: 更新真实 DOM]
        ├──> [Layout: 执行 useLayoutEffect]
        └──> [浏览器绘制]
        ↓
[Passive Effect Phase]
        └──> [执行 useEffect]
        ↓
[等待下一次 setState 或事件...]

理解这个流程,能帮助你:

  • 预测行为:知道为什么状态不会立即更新,为什么需要函数式更新。
  • 调试问题:定位状态更新不生效、无限循环渲染等问题的根源。
  • 优化性能:理解批量更新和并发渲染的原理,写出更高效的代码。
  • 设计架构:合理规划状态提升、状态管理库的使用。

setState 是 React 响应式更新的引擎,掌握其背后的机制,是成为 React 高手的关键一步。

相关推荐
遂心_2 小时前
JavaScript 函数参数传递机制:一道经典面试题解析
前端·javascript
小徐_23332 小时前
uni-app vue3 也能使用 Echarts?Wot Starter 是这样做的!
前端·uni-app·echarts
RoyLin2 小时前
TypeScript设计模式:适配器模式
前端·后端·node.js
遂心_3 小时前
深入理解 React Hook:useEffect 完全指南
前端·javascript·react.js
Moonbit3 小时前
MoonBit 正式加入 WebAssembly Component Model 官方文档 !
前端·后端·编程语言
龙在天3 小时前
ts中的函数重载
前端
卓伊凡3 小时前
非常经典的Android开发问题-mipmap图标目录和drawable图标目录的区别和适用场景实战举例-优雅草卓伊凡
前端
前端Hardy3 小时前
HTML&CSS: 谁懂啊!用代码 “擦去”图片雾气
前端·javascript·css
前端Hardy3 小时前
HTML&CSS:好精致的导航栏
前端·javascript·css