React 中 setState 是同步还是异步的?
结论
- React 17 及之前:在 React 管理的事件处理函数中是"异步"的,在 setTimeout/原生事件中是同步的
- React 18+:所有更新都是"异步"的(自动批处理)
React 17 行为
// React 17
function handleClick() {
console.log(count) // 0
setCount(count + 1)
console.log(count) // 0(异步,未立即更新)
setTimeout(() => {
setCount(count + 1)
console.log(count) // 1(同步,在 setTimeout 中)
}, 0)
}React 18 自动批处理
// React 18
function handleClick() {
setCount(c => c + 1) // 不会立即重渲染
setCount(c => c + 1) // 合并
setName('Tom') // 合并
// 一次重渲染,count = 2, name = 'Tom'
}
// setTimeout 中也批处理
setTimeout(() => {
setCount(c => c + 1)
setName('Tom')
// 一次重渲染
}, 0)
// Promise 中也批处理
fetch().then(() => {
setCount(c => c + 1)
setName('Tom')
// 一次重渲染
})批处理原理
React 将多个 setState 收集到更新队列,在当前事件循环结束后统一处理:
// 简化原理
let isBatching = false
let updateQueue = []
function setState(update) {
if (isBatching) {
updateQueue.push(update) // 加入队列
} else {
// 立即应用
}
}
function batch(fn) {
isBatching = true
fn()
isBatching = false
flushQueue() // 统一处理队列
}函数式更新
// ❌ 基于当前 count 值,连续调用会覆盖
setCount(count + 1)
setCount(count + 1)
// 结果:count + 1(只增加了 1)
// ✅ 函数式更新,基于前一个状态
setCount(prev => prev + 1)
setCount(prev => prev + 1)
// 结果:count + 2(正确增加了 2)flushSync 强制同步
import { flushSync } from 'react-dom'
function handleClick() {
flushSync(() => {
setCount(1) // 立即重渲染
})
// 此时 DOM 已更新
flushSync(() => {
setName('Tom') // 再次立即重渲染
})
}总结
- React 18 中 setState 都是批处理的(异步的)
- 使用函数式更新
setCount(prev => prev + 1)避免状态覆盖 - 需要 DOM 同步更新时使用
flushSync