react的 useState 原理、批量更新机制

useState 是 React Hooks 中最核心的 Hook 之一。

你可以把它理解成:

"函数组件中的状态存储器 + 更新调度器"

很多人会背:

scss 复制代码
const [state, setState] = useState(initialValue)

但真正面试或者深入开发时,更重要的是理解:

  1. useState 为什么能记住状态?
  2. 为什么 setState 后不会立刻更新?
  3. React 为什么会"批量更新"?
  4. React18 为什么有自动批量更新?
  5. 为什么连续 setCount(count + 1) 不会 +2?

下面系统讲。


一、useState 原理


1. useState 本质

本质上:

ini 复制代码
useState = 状态存储 + 更新函数

React 内部会为每个组件维护:

复制代码
fiber.memoizedState

里面像链表一样保存 Hook。

大概类似:

yaml 复制代码
{
  state: 0,
  queue: []
}

二、为什么函数组件每次执行还能记住状态?

看起来很奇怪:

javascript 复制代码
function App() {
  const [count, setCount] = useState(0)

  return <div>{count}</div>
}

函数每次重新执行:

复制代码
count 不应该重新变成 0 吗?

其实不会。

因为:

php 复制代码
状态不在函数里
状态在 React Fiber 对象里

React 会:

perl 复制代码
组件重新执行
↓
按顺序读取 hooks
↓
拿到上次保存的 state

三、Hook 为什么不能写在 if 里?

因为 React 是:

复制代码
按调用顺序记录 hooks

例如:

scss 复制代码
useState() // 第1个hook
useEffect() // 第2个hook
useState() // 第3个hook

React 靠"位置"识别 hook。

如果:

scss 复制代码
if (flag) {
  useState()
}

下一次顺序变了:

复制代码
hook 对不上

状态错乱。

所以:

复制代码
Hooks 必须顶层调用

四、setState 为什么是异步的?

例如:

scss 复制代码
const [count, setCount] = useState(0)

const click = () => {
  setCount(1)

  console.log(count)
}

输出:

复制代码
0

不是 1。

因为:

markdown 复制代码
setState 不会立刻修改 state
而是:
1. 先放入更新队列
2. React 调度更新
3. 重新渲染组件

五、useState 更新流程(核心)

React 内部大概:

scss 复制代码
setState()
↓
创建 update 对象
↓
放入 queue
↓
调度 render
↓
执行函数组件
↓
计算新 state
↓
更新 DOM

六、为什么连续 setCount 不会累加?

经典题:

scss 复制代码
const [count, setCount] = useState(0)

const click = () => {
  setCount(count + 1)
  setCount(count + 1)
}

结果:

复制代码
1

不是 2。

因为:

两次拿到的:

ini 复制代码
count === 0

等价于:

scss 复制代码
setCount(1)
setCount(1)

最终还是 1。


七、正确累加:函数式更新

应该:

ini 复制代码
setCount(prev => prev + 1)
setCount(prev => prev + 1)

结果:

复制代码
2

因为 React 会:

ini 复制代码
第一次:
prev = 0 → 1

第二次:
prev = 1 → 2

八、批量更新机制(重点)

React 为了性能:

复制代码
不会每次 setState 都立刻 render

而是:

复制代码
把多个更新合并
最后统一 render 一次

这就是:

Batch Update(批量更新)


九、React17 的批量更新

React17 中:

只有 React 事件内才批量更新。

例如:

scss 复制代码
const click = () => {
  setA(1)
  setB(2)
}

只 render 一次。


但:

scss 复制代码
setTimeout(() => {
  setA(1)
  setB(2)
})

会 render 两次。

因为:

arduino 复制代码
setTimeout 不在 React 控制范围

十、React18 自动批量更新

React18:

扩大了批量更新范围。

现在:

javascript 复制代码
setTimeout
promise
async/await
原生事件

也会自动批量更新。

例如:

scss 复制代码
setTimeout(() => {
  setA(1)
  setB(2)
})

React18:

复制代码
只 render 一次

十一、React18 为什么能做到?

React18 引入:

Concurrent Features(并发特性)

内部调度器更强。

React 能统一收集更新:

复制代码
微任务阶段统一处理

所以实现了:

复制代码
Automatic Batching

十二、什么时候会立即更新?

可以强制同步:

scss 复制代码
flushSync(() => {
  setCount(1)
})

来自:

javascript 复制代码
import { flushSync } from 'react-dom'

这会:

复制代码
立即 render

但一般少用。


十三、useState 底层数据结构(面试加分)

React Hook 内部:

复制代码
单向链表

Fiber 中:

复制代码
fiber.memoizedState

每个 Hook 节点:

yaml 复制代码
{
  memoizedState: 当前状态,
  queue: 更新队列,
  next: 下一个hook
}

十四、简化版 useState 实现(理解核心)

可以自己模拟:

scss 复制代码
let state
function useState(initialValue) {
  state = state || initialValue

  function setState(newValue) {
    state = newValue
    render()
  }

  return [state, setState]
}

真正 React 当然复杂很多:

  • Fiber
  • Hook 链表
  • 优先级
  • 更新队列
  • 调度器
  • 并发更新

但核心思想类似。


十五、面试高频题总结


1. useState 是同步还是异步?

答:

复制代码
setState 本身不是异步 API
而是 React 的状态更新是"调度式"的
表现上像异步

2. 为什么 state 不立刻更新?

因为:

复制代码
React 要做批量更新优化

3. 为什么 Hook 不能放 if?

因为:

复制代码
React 按 hook 调用顺序记录状态

4. 为什么 useState 能记住状态?

因为:

php 复制代码
状态存在 Fiber,不在函数里

5. React18 批量更新变化?

React18:

javascript 复制代码
自动批量更新扩大到:
setTimeout
Promise
async/await
原生事件

十六、真正理解一句话(最重要)

可以记住:

复制代码
React 不是"调用 setState 修改变量"

而是:

提交一个更新任务给 React 调度器

这是理解 React 更新机制的核心。


十七、完整流程图(非常重要)

arduino 复制代码
点击按钮
   ↓
setState
   ↓
创建 update
   ↓
放入 queue
   ↓
React Scheduler 调度
   ↓
render 阶段
   ↓
重新执行函数组件
   ↓
生成新的 Virtual DOM
   ↓
diff
   ↓
commit
   ↓
更新真实 DOM

十八、一个经典面试题

scss 复制代码
function App() {
  const [count, setCount] = useState(0)

  console.log("render")

  return (
    <button
      onClick={() => {
        setCount(count + 1)
        setCount(count + 1)

        Promise.resolve().then(() => {
          setCount(count + 1)
        })
      }}
    >
      {count}
    </button>
  )
}

React18 下点击后:

最终:

ini 复制代码
count = 1

因为:

所有拿到的 count 都是:

复制代码
当前 render 快照里的 0

如果改成:

ini 复制代码
setCount(c => c + 1)

最终:

ini 复制代码
count = 3

这就是:

"闭包快照" + "批量更新"

的核心。

相关推荐
叫我少年1 小时前
Markdown 备忘清单
前端
酒吧舞高材生1 小时前
vue3 PC端-索引列表组件
前端·vue.js
2301_780789661 小时前
多层级 CC 防护体系:前端验证与后端限流的协同配置实践
运维·服务器·前端·网络安全·智能路由器·状态模式
ZC跨境爬虫1 小时前
跟着MDN学HTML_day_47:(Document接口)
前端·javascript·ui·html·ecmascript·音视频
Swift社区1 小时前
Flutter / React / ArkUI:在鸿蒙 PC 上怎么选?
flutter·react.js·harmonyos
学习论之费曼学习法1 小时前
ReAct框架深度解析:让Agent会思考再行动
前端·react.js·前端框架
前端 贾公子1 小时前
从零开始:使用Node.js和Cheerio进行轻量级网页数据提取
前端·vue.js
阿星做前端1 小时前
不想再给ai回复下一步了,于是我给agent装上了一个自动挡
前端·后端·程序员