react组件
react组件本质就是一个函数,return 返回的jsx构成视图。
视图可以使用 函数内部 let const useState Props 的值。
let const useState Props
哪些值的改变会让组件函数重新执行(也就是组件重新渲染呢?)只有useStateProps的值 发生改变之后 组件才会重新渲染
useState的用法
关键词
setState 进入队列 异步更新
useState值的改变要 使用set函数 整体替换
React 不会因为对象内部变化而重新渲染,必须传入新对象引用 才能让 React 感知到变化。
useState值改变之后,组件会重新渲染,这个过程是怎样的!
🧩 一、React 内部的更新队列机制示意
当你调用 setState 时,React 并不会马上修改 state,而是像这样往队列里加"更新任务":
🔴 情况一:普通写法(值更新)(三个setCount中的count都是闭包里的旧值)
scss
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
假设当前 count = 0。
React 收到的其实是:
yaml
更新队列:
[ { value: 1 }, { value: 1 }, { value: 1 } ]
然后 React 最终会这样处理:
ini
初始 state: 0
处理更新 1 → newState = 1
处理更新 2 → newState = 1(没变化)
处理更新 3 → newState = 1(没变化)
🧨 所以最后只会变成 1。
因为这三个更新都基于同一个旧值(0)计算出来。
🟢 情况二:函数式更新(函数更新)
ini
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
React 收到的更新队列变成这样:
javascript
更新队列:
[ { updater: (prev) => prev + 1 },
{ updater: (prev) => prev + 1 },
{ updater: (prev) => prev + 1 } ]
React 在真正更新时会顺序执行每个函数:
ini
初始 state: 0
执行第一个 updater → prev = 0 → newState = 1
执行第二个 updater → prev = 1 → newState = 2
执行第三个 updater → prev = 2 → newState = 3
✅ 最终结果:count = 3
⚙️ 三、这说明了什么?
| 对比点 | 普通更新 | 函数式更新 |
|---|---|---|
| 更新类型 | 写死的值 | 延迟执行的函数 |
| React 处理顺序 | 只保留最后一个值 | 顺序执行每个函数 |
| 最终结果 | 只更新一次 | 连续叠加 |
💡 类比理解
可以把 setCount(xxx) 想象成往"任务列表"中丢任务。
- 普通写法:丢进去的是「我想把值改成 1」
- 函数式写法:丢进去的是「等会执行我,传给我最新值,我来算新值」
第二种方式才真正能保证逻辑正确。
看看下面最终的结果是啥?
js
function App() {
const [user, setUser] = useState({name: '张三', age: 18})
const handleClick = () => {
setUser({...user, name: '李四'})
setUser({...user, age: 20})
}
return (
<div className="app">
<FileUpload />
<button onClick={handleClick}>click</button>
<p>{user.name}</p>
<p>{user.age}</p>
</div>
)
}
点击click之后的结果是张三,20.
原因:setState不是修改 闭包中的老值,而是根据这个老值生成新的state,所以老的state不会被修改
函数式更新是 把 生成的新state传递下去,让下面的set函数,根据自己新state来操作。
(不是函数式更新,只看最后一次setState即可)
setSate是异步的,只有队列执行完之后才会 重新渲染组件
1️⃣ setState 的本质
-
普通写法(直接值)
scsssetCount(count + 1)- 把计算好的"值"放入更新队列。
- 如果多次调用,每次都是基于调用时的旧值计算的。
-
函数式写法(函数)
inisetCount(prev => prev + 1)- 把函数放入更新队列,等队列执行时会拿到"最新的 state"作为参数。
- 可以连续、正确地叠加更新。
2️⃣ 更新队列处理流程
React 内部维护一个 更新队列(update queue) ,处理步骤:
-
调用 setState → React 不立即修改 state,而是把更新任务加入队列。
-
批量处理队列:
- React 会按顺序处理队列里的任务。
- 对于普通值更新,后面的更新可能覆盖前面,导致丢失变化。
- 对于函数式更新,每个 updater 都能拿到最新值计算。
-
队列处理完毕→ React 决定是否触发重新渲染。
3️⃣ 渲染时机
-
批量更新(Batching) :在 React 的事件回调中(例如
onClick),多次setState会被合并成一次渲染。 -
队列执行完毕后:
- React 会拿最新的 state 计算组件要渲染的虚拟 DOM。
- 与上一次虚拟 DOM 做 diff。
- 最后把差异更新到真实 DOM。
-
所以,你可以理解为:
只有队列里的所有更新都执行完,React 才会触发组件重新渲染。
在 React 事件处理函数 中:
即使你连续调用多次
setState(包括函数式更新),React 只会触发一次重新渲染。
🔍 一、为什么只渲染一次?
因为 React 有一个「批量更新机制(Batching) 」。
当你在事件处理函数中写:
scss
function handleClick() {
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
}
React 内部流程如下👇:
- 进入事件回调 → React 开启批量更新模式;
- 每次
setCount不会立即触发渲染,只是往队列里加更新; - 当事件函数执行完毕后,React 才合并所有更新;
- 执行完所有 updater(函数式更新),得到最终的 state;
- 最后只 重新 render 一次。
🧩 二、具体过程模拟
scss
const [count, setCount] = useState(0)
function handleClick() {
setCount(prev => prev + 1) // 加入队列
setCount(prev => prev + 1) // 加入队列
setCount(prev => prev + 1) // 加入队列
console.log('after set:', count)
}
输出过程大致如下:
ini
点击前:count = 0
点击时:三个 setCount 都加入队列,但组件还没重新渲染
console.log 打印:0 (旧值)
React 合并更新,计算最终 state = 3
React 重新 render 一次(只一次!)
⚙️ 三、那什么时候会 render 多次?
如果你把这些更新放到不同的异步回调里,比如:
scss
setTimeout(() => setCount(prev => prev + 1), 0)
setTimeout(() => setCount(prev => prev + 1), 0)
setTimeout(() => setCount(prev => prev + 1), 0)
此时每个 setTimeout 都是独立的事件上下文,React 不会把它们批量合并。
于是:
- 每次
setCount都会单独触发一次渲染; - 最终渲染了 3 次。
🧩 四、总结
| 场景 | 是否批量更新 | 渲染次数 |
|---|---|---|
| 同一个事件回调里多次 setState | ✅ 是 | 1 次 |
| 不同异步回调(setTimeout、Promise.then) | ❌ 否 | 多次 |
| 使用 React 18 自动批量(启用 Concurrent Mode) | ✅ 是(几乎所有情况) | 1 次 |
✅ 一句话总结:
React 会在一个同步事件中收集所有
setState,等函数执行完再合并,所以你即使执行三次更新,也只会 render 一次。
setTimeout(() => setCount(prev => prev + 1), 0) 为什么setTimeout有独立的事件上下文
🧠 一、JavaScript 事件循环与调用栈
JS 是单线程执行的。浏览器维护一个事件循环(event loop):
rust
主线程执行:
同步代码 -> 执行完 -> 检查任务队列 -> 执行下一个宏任务 / 微任务
所以:
- 同步任务(比如普通的点击事件函数)都在同一个调用栈中;
- 异步任务 (比如
setTimeout回调)会被放到任务队列中,等同步任务执行完之后再执行。
⚙️ 二、React 的批量更新机制(Batching)
React 内部有一个"批量更新模式"的开关,伪代码如下:
scss
ReactDOM.flushSync(() => {
// 批量更新开启
setCount(1)
setName('foo')
// ...
// 批量更新关闭
})
这个"开关"只在某些 React 管控的上下文中 打开,比如:
- 组件生命周期(render、effect、事件回调);
- React 事件系统触发的事件(如 onClick、onChange)。
当批量更新模式开启时:
- 所有
setState不会立即触发渲染; - React 会收集所有更新,事件结束后统一执行一次。
🧩 三、为什么 setTimeout 不在 React 的上下文里?
当你这样写:
scss
function handleClick() {
setTimeout(() => setCount(c => c + 1), 0)
}
执行过程是:
- 你点击按钮 → React 调用
handleClick; setTimeout注册了一个浏览器原生异步任务;handleClick执行完 → React 的批量模式关闭;- 浏览器主线程空闲时,执行
setTimeout回调; - 此时 React 不再处于"批量模式",所以
setState立刻触发一次渲染。
⚠️ 因此:
每个
setTimeout的回调都在「React 批量更新范围外」,所以每次都单独触发 render。
🧩 四、例子对比一下
scss
// ✅ 批量更新(1次渲染)
function handleClick() {
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
}
// ❌ 非批量更新(3次渲染)
function handleClick() {
setTimeout(() => setCount(prev => prev + 1), 0)
setTimeout(() => setCount(prev => prev + 1), 0)
setTimeout(() => setCount(prev => prev + 1), 0)
}
第一个例子里,所有更新都在 React 控制的事件回调内 → React 自动合并。
第二个例子里,setTimeout 回调是原生异步上下文 → React 不知道你要合并。
🧩 五、React 18 之后的变化:自动批量(Automatic Batching)
在 React 18(启用 concurrent 模式)中,React 扩展了批量更新范围:
即使在
setTimeout、Promise.then、fetch等异步回调中调用setState,React 也会自动合并更新 🎉。
例如:
scss
setTimeout(() => {
setCount(c => c + 1)
setName('Alice')
}, 0)
在 React 18 中,这两个更新会被合并成一次渲染 。
(以前的版本是两次)
🧭 六、总结表格
| 场景 | React 17及以前 | React 18(自动批量) |
|---|---|---|
同一个事件回调中多次 setState |
✅ 合并更新 | ✅ 合并更新 |
不同 setTimeout 回调中多次 setState |
❌ 各自渲染 | ✅ 自动合并 |
| Promise.then / fetch / async 回调 | ❌ 各自渲染 | ✅ 自动合并 |
🎬 一、先认识两个关键角色
1️⃣ React 的批量更新开关(Batch Mode)
React 在某些场景会打开"批量模式":
"所有
setState我先记着,不急着更新,等函数结束我一起更新。"
比如:
- React 事件回调(
onClick,onChange) - 生命周期函数(
useEffect,useLayoutEffect)
当事件结束后,React 会自动关闭批量模式,然后统一触发一次重新渲染。
2️⃣ 浏览器的事件循环机制
浏览器的事件循环分两种任务:
- 同步任务(主线程执行)
- 异步任务(放入任务队列,稍后执行)
setTimeout 的回调就是被放进"任务队列"的异步任务。
它不会立刻执行,要等主线程的同步代码都跑完再轮到它。
🧩 二、我们用时间轴看整个过程
来看代码 👇
scss
function handleClick() {
setTimeout(() => setCount(prev => prev + 1), 0)
}
然后点击按钮。
🕒 Step 1:React 调用事件回调
React 捕获到点击事件 → 调用 handleClick()。
此时 React 打开了"批量模式":
ini
ReactBatching = true
🕒 Step 2:执行 setTimeout 注册异步任务
执行到:
scss
setTimeout(() => setCount(...), 0)
这里其实什么都没更新,只是:
"告诉浏览器:我有个任务,等同步任务都执行完再来执行它。"
于是浏览器把回调放到任务队列:
ini
TaskQueue = [() => setCount(...)]
🕒 Step 3:handleClick 执行结束
handleClick() 执行完毕。
React 认为:"当前事件结束了,我可以把批量模式关掉。"
ini
ReactBatching = false
🕒 Step 4:浏览器执行 setTimeout 回调
主线程空闲后,浏览器取出任务队列里的回调执行:
ini
setCount(prev => prev + 1)
但此时 React 并不知道这是"同一个事件"中的更新了,
因为 React 的批量模式已经关了!
于是 React 直接:
"好,我立刻更新 state 并重新渲染一次组件。"
✅ 最终结果
每次进入 setTimeout 的回调,都独立触发一次渲染。
因为这些回调是在 React 的"批量模式"之外执行的。
🧠 三、用动画类比理解
可以想象成:
| 阶段 | React 批量模式 | 状态 |
|---|---|---|
| 点击按钮 → 进入 handleClick | ✅ 打开批量模式 | "收集更新" |
| handleClick 结束 | ❌ 关闭批量模式 | "该渲染了" |
| 浏览器稍后执行 setTimeout 回调 | 🚫 React 已不知情 | "立刻渲染一次" |
🧩 四、如果想让它在异步回调里也批量更新怎么办?
在 React 18 之后(Concurrent Mode),React 扩展了批量机制:
即使是在
setTimeout、Promise.then等异步中,React 也会自动帮你批量。
所以在 React 18 中:
scss
setTimeout(() => {
setCount(c => c + 1)
setName('Alice')
}, 0)
只会触发 1 次渲染 ✅。
哪些操作会触发批量更新
1️⃣ 什么是批量更新(Batching)
React 的 批量更新模式 ,本质上是一个开关,控制 同一次事件中多次 setState 是否合并渲染。
- 批量模式开着:多次
setState只触发 一次 render - 批量模式关掉:每次
setState都会单独触发渲染
伪代码示意:
scss
ReactBatching = true // 开启批量
setCount(1)
setName('foo')
// 事件结束,React 会合并更新
render()
ReactBatching = false // 关闭批量
2️⃣ 触发批量更新的上下文
React 并不是所有 setState 都批量,而是只在 React 控制的上下文 才批量:
(1) React 事件系统触发的事件
- 如
onClick,onChange,onInput - React 内部会把事件回调包装成 SyntheticEvent
- 批量模式在回调执行前自动打开,回调执行完毕后自动关闭
- 示例:
scss
<button onClick={() => {
setCount(c => c + 1)
setName('Alice')
}}>Click</button>
✅ 同一事件回调里的两个 setState 会被批量,最终只 render 一次。
(2) 组件生命周期
render内部不会调用setState(会报错),但useEffect/useLayoutEffect可以- React 会在这些钩子回调中 默认开启批量模式
- 例如:
scss
useEffect(() => {
setCount(c => c + 1)
setName('Bob')
}, [])
✅ 两次更新也只触发一次 render。
(3) React 18 扩展后的异步上下文
- React 18 引入 Automatic Batching
- 批量模式扩展到 几乎所有异步场景 ,包括
setTimeout、Promise.then、fetch - 示例:
scss
setTimeout(() => {
setCount(c => c + 1)
setName('Charlie')
}, 0)
在 React 18 下,也会合并成 一次 render
注意:如果是 React 17,setTimeout 内的更新不会被合并。
3️⃣ React 内部是如何实现的
核心就是一个 批量标志 + 更新队列:
scss
let isBatching = false
let updateQueue = []
function setState(update) {
if (isBatching) {
updateQueue.push(update) // 记录更新
} else {
// 立即更新
state = typeof update === 'function' ? update(state) : update
render()
}
}
function flushUpdates() {
for (const u of updateQueue) {
state = typeof u === 'function' ? u(state) : u
}
render()
updateQueue = []
}
React 事件回调执行时:
ini
isBatching = true
callback()
flushUpdates()
isBatching = false
flushUpdates()在事件结束时一次性把队列里的更新合并- 批量模式自动控制渲染次数
4️⃣ 总结理解
| 方面 | 描述 |
|---|---|
| 批量更新 | 同一事件或同一上下文内,多次 setState 只渲染一次 |
| React 17 默认 | 仅事件回调 & 生命周期钩子中批量,异步回调不批量 |
| React 18 自动批量 | 批量范围扩展到几乎所有异步回调(setTimeout、Promise 等) |
| 原理 | isBatching 标志 + 更新队列,事件结束 flush 一次 |
简单理解:
批量更新模式就是 React 的开关,控制 setState 是否"收集起来一次渲染"。
事件回调、effect、生命周期里开,其他普通异步可能不开(React 17),React 18 自动扩展。