React useState 内部原理
React useState 的内部原理,state 快照模型与更新队列机制。
#type / concept
#status / growing
#tech / dev / frame
#resource / react
[!info] related notes
- 所属 MOC: React MOC
- 前置概念: React useState
- 并列概念: React useEffect 内部原理
- 易混淆概念: Vue3 响应式是 Proxy,React 是快照模型
- 关系笔记: React 状态模型 vs Vue3 响应式模型
React useState 内部原理
一句话定义
React 的 useState 不是”直接修改变量”,而是”提交状态更新到队列,由 React 在后续渲染中处理”。
核心内容
状态快照模型
函数组件中的 state 不是”可变的变量”,而是”当前渲染的快照”:
const [count, setCount] = useState(0)
setCount(count + 1)
console.log(count) // 这里还是旧值,不是 1
理解关键点:
- 调用
setCount()是提交更新,不是原地修改 - 提交后当前渲染上下文里的
count不会立刻变化 - React 会在后续安排新渲染,新渲染里才会拿到新值
简化的内部伪代码
// 简化的心智模型
let state = 0
let queue = []
function useState(initialValue) {
state = state !== undefined ? state : initialValue
function setState(action) {
queue.push(action)
scheduleRender() // 安排后续渲染
}
return [state, setState]
}
function processQueue() {
let nextState = state
for (const action of queue) {
nextState = typeof action === 'function'
? action(nextState)
: action
}
state = nextState
queue = []
}
函数式更新的作用
// 问题:两次都基于同一个旧值,结果只是 +1
setCount(count + 1)
setCount(count + 1)
// 解决:使用函数,React 会依次执行,结果是 +2
setCount(n => n + 1)
setCount(n => n + 1)
batchUpdate 批量更新
React 会把多个 setState 合并成一次渲染:
handleClick = () => {
setCount(c => c + 1)
setFlag(f => !f)
// React 会合并成一次渲染
}
边界与易混淆点
React 为什么强调不可变更新
React 的模型是”提交新状态,替换旧状态”:
// 需要这样写
setUser(prev => ({ ...prev, name: 'Tom' }))
// 而不是
user.name = 'Tom' // 这样不会触发更新
因为 React 需要通过”新旧是否不同”来判断是否需要重新渲染。
和 Vue3 响应式的核心差异
| 方面 | React useState | Vue3 响应式 |
|---|---|---|
| 更新方式 | 提交到队列 | 直接修改 |
| 读取时机 | 同步读取当前快照 | 同步读取修改后的值 |
| 依赖收集 | 组件级,依赖数组 | 自动细粒度追踪 |
| 不可变更新 | 必须 | 不必须 |
异步渲染中的表现
在合成事件、生命周期中,setState 默认是批量异步的:
handleClick = () => {
setCount(c => c + 1)
console.log(count) // 仍然是旧值
}
在 setTimeout、原生事件中,默认是同步的:
useEffect(() => {
setCount(c => c + 1)
console.log(count) // 可能是新值(取决于 React 18 并发)
}, [])