我们结合上一篇文章React 执行阶段与渲染机制详解(基于 React 18+ 官方文档)React 执行阶段与渲染机制详解(基于 Rea - 掘金梳理的 React 执行阶段(Render Phase, Commit Phase, Passive Effect Phase)和状态管理机制,来深入剖析 useState
产生的 setState
的完整执行流程。
理解 setState
的行为是掌握 React 状态管理的核心。它远非简单的"设置一个值",而是一个触发复杂协调和渲染流程的起点。
🧩 useState
与 setState
的核心概念
首先,明确几个关键点:
useState
是 Hook :在函数组件首次渲染时调用,返回一个状态变量和一个setState
函数。setState
是调度器 (Scheduler) :调用setState
不会立即更新状态,也不会立即触发重新渲染。它只是向 React 的调度系统"请求"一次状态更新。- 状态更新是异步且可能批量的 :React 会为了性能,将多个
setState
调用合并成一次更新,尤其是在事件处理器中。 - 状态是不可变的快照 :每次渲染,组件函数都会从头开始执行,拿到的是当前渲染周期的
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)在合适的时机(通常是当前浏览器任务结束后,或根据并发模式的优先级)开始处理更新队列。
- 内部动作 :
- 计算新状态 :
- React 会处理该组件 Fiber 节点上的所有待定更新。
- 如果
setState
传递的是函数 (如setScore(prev => prev + 1)
),React 会按顺序调用这些函数,将上一个函数的返回值作为下一个函数的参数,最终得到一个新值。 - 如果传递的是值 (如
setScore(5)
),则直接使用该值(如果多次调用,后面的会覆盖前面的,除非是函数形式)。
- 触发组件重新渲染 :
- React 使用计算出的新状态 ,重新执行您的整个函数组件。
- 在这次新的执行中,
useState
Hook 会返回这个新的状态值。 - 组件根据新的
state
和props
生成新的 JSX(Virtual DOM)。
- Reconciliation (协调) :
- React 将新生成的 Virtual DOM 树与上一次渲染的树进行比较(Diff 算法)。
- 计算出最小的、必要的 DOM 变更列表。
- 可中断性:在 Concurrent Mode 下,这个阶段是可以被更高优先级的任务(如用户输入)中断的。React 可能会丢弃当前的渲染结果,稍后再重新开始。
- 计算新状态 :
第 3 步:进入 Commit Phase(提交阶段)
- 发生时机:Render Phase 成功完成,计算出最终的变更列表后。
- 内部动作 :
- Mutation (变更) :React 根据 Diff 结果,同步地更新真实的 DOM。这是用户能看到界面变化的时刻。
- Layout Effects :执行所有
useLayoutEffect
的清理函数(来自上一次渲染),然后执行本次渲染的useLayoutEffect
创建函数。这些是同步执行的,发生在浏览器绘制之前,可以用来读取或同步修改刚刚更新的 DOM。 - 浏览器绘制:浏览器根据更新后的 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>
);
}
调用第二个 Counter
的 setScore
只会影响它自己的状态。当它被卸载时,其状态会被销毁。重新挂载时,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 高手的关键一步。