React 中 setState 是同步还是异步的?

结论

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')  // 再次立即重渲染
  })
}

总结

同分类其他题目