React 中的 State 执行机制与渲染过程
约 1559 字大约 5 分钟
React
2025-08-07
状态更新不是修改,而是“重新指向”
首先必须理解一个核心点:React 的 state 是不可变的。
当你调用:
const [count, setCount] = useState(0);
setCount(count + 1);
React 并不会去修改原来的 count
值,而是创建一个新的状态值(比如 1),并将内部“状态指针”指向这个新值。这就像这样:
[0] → [1] → [2] → ...
↑
当前状态指针指向的值
如果旧值没有被手动保留,JavaScript 引擎的 GC(垃圾回收)机制就会在适当时机将其回收。
状态变更触发渲染,生成新的“UI 快照”
当 setState
被调用后,React 并不会立刻重新渲染组件,而是:
- 标记该组件为“需要更新”
- 将新的状态值排入更新队列(这是批处理的基础)
- 等待 React 调度新的渲染任务(在浏览器空闲时或下一轮事件循环)
然后 React 会重新执行这个函数组件 —— 也就是执行一次“渲染函数”:
function Counter() {
const [count, setCount] = useState(1);
return <div>{count}</div>;
}
此时 React 会根据 最新的 state + props 生成新的虚拟 DOM,也就是所谓的“UI 快照”。
它会与旧的虚拟 DOM 进行 diff,然后将差异通过 Fiber 树的提交阶段 应用到真实 DOM,完成页面更新。
渲染过程中的 state 是“快照式”固定的
这是 React 函数组件中一个极其重要的原则:
在一次渲染(即一次函数执行)中,state 是“固定”的,不会因为后续的 setState
而变化。
即使你在一次函数中多次调用:
setCount(count + 1);
setCount(count + 1);
由于 count
是这次渲染中的旧值,两次 setCount(count + 1)
都是以同一个旧值为基础的,因此你不会得到递增两次的结果。
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // count 是 0
setCount(count + 1); // count 还是 0
}
最终只会更新为 1
而不是 2
。这就是因为在当前渲染周期中,count 是不可变的状态快照。
正确写法应该是基于上一个状态:
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 最终会加到 2
这也是为什么 React 提供了 更新函数版本(即传入函数)的 setState。
为什么 state 不变才能保证一致性?
你可以把 useState
理解成 React 内部维护的一份“状态仓库”。当组件被 React 调用执行渲染函数时,React 会:
- 使用 Fiber 节点中的“当前状态快照”
- 每次执行
useState
时,从这份快照中“顺序读取”当前值 - 生成虚拟 DOM 并提交更新
如果在渲染过程中状态值是动态变化的,那么组件就无法保证“同一份状态值生成同一份 UI”,这会导致极其复杂且难以预测的副作用。
因此,React 的设计原则之一是:
“状态只在下一次渲染时生效,且当前渲染期间不可变”。
从状态变更到 UI 更新的流程
用户调用 setState()
↓
React 将新状态排入更新队列
↓
调度器安排一次新的渲染
↓
函数组件被重新执行(渲染函数)
↓
useState 读取的是当前最新值(状态快照)
↓
React 根据 state + props 生成新的虚拟 DOM
↓
与旧虚拟 DOM 进行 diff
↓
生成最小的变更操作并提交到真实 DOM
↓
完成 UI 更新
setState的异步调度和批处理
当你调用:
setCount(count + 1);
React 并不是立刻去修改 count
,也不会立刻重新渲染组件,而是把这个“更新请求”加入内部的更新队列中,等到当前事件处理完毕后,再批量调度一次渲染。
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
}
假设当前 count
是 0,调用这两个 setCount,其实你提交了两个值都是 1
,React 最终只会把最后一个 1
用来渲染:
这就是因为:
在一次同步执行(比如点击事件)的过程中,count
是当前渲染的快照值,不会随着 setCount
立即更新,所以你连续两次 setCount(count + 1)
实际上传入的参数是一样的。
你需要使用更新函数版本的 setCount
才能实现真正的“加两次”:
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1) → prev = 0 → new = 1
setCount(prev => prev + 1) → prev = 1 → new = 2
最终 count
就是 2。
- State 是不可变快照
- setState 是异步批量更新
- 事件处理期间不会触发同步渲染
- 更新函数(functional update)可避免闭包陷阱
调用 setCount
不会立即更新 count
,而是将更新请求加入队列。直到下一次渲染开始时,React 才会处理这些更新,并使用新值重新渲染组件。连续多次调用时,如不使用函数式更新,会因为闭包快照导致值重复。
同一次渲染中多次调用setXXX
React 的内部在调用 setState
时,判断了你传入的是函数还是值,并分别走了两套逻辑,这正是 “函数式更新(functional update)” 的本质。
setCount(prev => prev + 1); // 传入的是“函数”
React 内部会记录这个“更新函数”,在稍后的更新队列处理阶段,它会把当前的最新 state 作为 prev
传给这个函数,然后计算出新的值。
// 假设当前 state 是 0
setCount(prev => prev + 1); // 计算结果是 1
setCount(prev => prev + 1); // 再用上一次结果 1,计算出 2
React 的 setState
支持两种更新方式:传入值(直接更新)和传入函数(基于上一次状态更新)。React 内部会判断参数类型,函数式更新允许你安全地进行多次累加、避免闭包陷阱,并确保每次都基于最新状态计算。
当你传入的是函数(即 setState(prev => ...)
),React 会延迟执行这些函数直到下一次渲染前,并依次用上一次更新的结果作为下一次的 prev
,最终得到最新状态值。这种机制让多次调用也能正确累加。
贡献者
更新日志
7eec2
-Document organization于9de0b
-全局优化于b1c4a
-文档迁移于