深入剖析 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 高手的关键一步。

相关推荐
To_OC18 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic19 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy19 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志19 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble20 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯21 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜1 天前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司1 天前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
江华森1 天前
Web 开发基础:HTTP 与 REST
前端·网络协议·http
IT_陈寒1 天前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端