React useState 内部原理

React useState 的内部原理,state 快照模型与更新队列机制。

#type / concept #status / growing #tech / dev / frame #resource / react

[!info] related notes

React useState 内部原理

一句话定义

React 的 useState 不是”直接修改变量”,而是”提交状态更新到队列,由 React 在后续渲染中处理”。

核心内容

状态快照模型

函数组件中的 state 不是”可变的变量”,而是”当前渲染的快照”:

const [count, setCount] = useState(0)

setCount(count + 1)
console.log(count) // 这里还是旧值,不是 1

理解关键点:

  1. 调用 setCount() 是提交更新,不是原地修改
  2. 提交后当前渲染上下文里的 count 不会立刻变化
  3. 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 useStateVue3 响应式
更新方式提交到队列直接修改
读取时机同步读取当前快照同步读取修改后的值
依赖收集组件级,依赖数组自动细粒度追踪
不可变更新必须不必须

异步渲染中的表现

在合成事件、生命周期中,setState 默认是批量异步的:

handleClick = () => {
  setCount(c => c + 1)
  console.log(count) // 仍然是旧值
}

在 setTimeout、原生事件中,默认是同步的:

useEffect(() => {
  setCount(c => c + 1)
  console.log(count) // 可能是新值(取决于 React 18 并发)
}, [])
创建于 2026/4/7 更新于 2026/5/27