第 42 题 setState 是同步还是异步?为什么?(批处理 batching、优先级)

20 阅读2分钟

React 面试题详细答案 - 第 42 题

42. setState 是同步还是异步?为什么?(批处理 batching、优先级)

setState 的执行机制

React 17 及之前:批处理机制
// React 17 及之前:事件处理器中的 setState 是异步的
function Component() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    console.log('点击前:', count) // 0

    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)

    console.log('点击后:', count) // 仍然是 0,因为 setState 是异步的

    // 实际执行:count 只会增加 1,因为多个 setState 会被批处理
  }

  return <button onClick={handleClick}>Count: {count}</button>
}
React 18:自动批处理
// React 18:所有 setState 都是异步的,包括 Promise、setTimeout 等
function Component() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    // 事件处理器中的批处理
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    // 结果:count 只增加 1
  }

  const handleAsync = () => {
    // React 18:异步操作中的批处理
    setTimeout(() => {
      setCount(count + 1)
      setCount(count + 1)
      setCount(count + 1)
      // 结果:count 只增加 1
    }, 1000)
  }

  const handlePromise = () => {
    // React 18:Promise 中的批处理
    fetch('/api/data').then(() => {
      setCount(count + 1)
      setCount(count + 1)
      setCount(count + 1)
      // 结果:count 只增加 1
    })
  }

  return (
    <div>
      <button onClick={handleClick}>同步批处理</button>
      <button onClick={handleAsync}>异步批处理</button>
      <button onClick={handlePromise}>Promise 批处理</button>
      <p>Count: {count}</p>
    </div>
  )
}

批处理的原理

批处理机制
// 简化的批处理实现
class ReactBatching {
  constructor() {
    this.isBatching = false
    this.pendingUpdates = []
  }

  // 开始批处理
  startBatching() {
    this.isBatching = true
  }

  // 结束批处理
  endBatching() {
    this.isBatching = false
    this.flushUpdates()
  }

  // 添加更新
  addUpdate(update) {
    if (this.isBatching) {
      this.pendingUpdates.push(update)
    } else {
      this.flushUpdates([update])
    }
  }

  // 执行更新
  flushUpdates(updates = this.pendingUpdates) {
    this.pendingUpdates = []
    updates.forEach((update) => update())
  }
}

// 使用示例
const batching = new ReactBatching()

function setState(newState) {
  batching.addUpdate(() => {
    // 执行状态更新
    applyStateUpdate(newState)
  })
}

// 事件处理器中的批处理
function handleClick() {
  batching.startBatching()

  setState({ count: 1 })
  setState({ count: 2 })
  setState({ count: 3 })

  batching.endBatching() // 只执行一次更新
}
优先级机制
// React 的优先级系统
const PriorityLevels = {
  Immediate: 1, // 立即执行
  UserBlocking: 2, // 用户阻塞
  Normal: 3, // 普通优先级
  Low: 4, // 低优先级
  Idle: 5, // 空闲时执行
}

// 不同优先级的更新
function Component() {
  const [count, setCount] = useState(0)
  const [urgent, setUrgent] = useState(false)

  const handleClick = () => {
    // 普通优先级更新
    setCount(count + 1)

    // 高优先级更新
    setUrgent(true)

    // 低优先级更新
    startTransition(() => {
      setCount(count + 1000) // 这个更新会被延迟
    })
  }

  return (
    <div>
      <button onClick={handleClick}>更新</button>
      <p>Count: {count}</p>
      <p>Urgent: {urgent ? 'Yes' : 'No'}</p>
    </div>
  )
}

实际应用示例

避免状态更新问题
// 问题:直接使用 state 值
function Counter() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1) // 使用当前 count 值
    setCount(count + 1) // 仍然使用旧的 count 值
    setCount(count + 1) // 仍然使用旧的 count 值
    // 结果:count 只增加 1
  }

  return <button onClick={handleClick}>Count: {count}</button>
}

// 解决方案:使用函数式更新
function Counter() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount((prev) => prev + 1) // 使用前一个值
    setCount((prev) => prev + 1) // 使用前一个值
    setCount((prev) => prev + 1) // 使用前一个值
    // 结果:count 增加 3
  }

  return <button onClick={handleClick}>Count: {count}</button>
}
强制同步更新
// 使用 flushSync 强制同步更新
import { flushSync } from 'react-dom'

function Component() {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)

  const handleClick = () => {
    // 强制同步更新
    flushSync(() => {
      setCount(count + 1)
    })

    // 这里 count 已经更新
    console.log('Count after flushSync:', count)

    // 普通更新
    setFlag(!flag)
  }

  return (
    <div>
      <button onClick={handleClick}>更新</button>
      <p>Count: {count}</p>
      <p>Flag: {flag ? 'Yes' : 'No'}</p>
    </div>
  )
}