React 状态更新机制深度解析:为什么连续调用 setCount 不能累加?

209 阅读2分钟

问题现象

在 React 组件中,我们经常会看到这样的代码:

const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

return (
<>
     <p>当前记数{count}</p>
      <button onClick={handleClick}>+3</button>
 </>
)

我们发现点击按钮后,count并没有按我们预想的那样为3,而是为1。

这是为什么呢?我们来仔细看看它的执行流程。

执行流程

React 中 setCount 的执行流程如下(以当前代码为例):

  1. 事件触发 :点击按钮调用 handleClick 函数
  2. 批量更新阶段 :
    • React 将三个 setCount(count + 1) 放入更新队列(此时 count 仍是初始值 0)
    • 由于是同步代码,React 会合并这些更新(性能优化)
  3. 调度阶段 :
    • React 将状态更新标记为待处理
    • 安排重新渲染(通过 React 的调度机制)
  4. 渲染阶段 :
    • React 计算最终状态:由于三次 setCount 都基于相同初始值,最终只执行一次 count + 1
    • 生成新的虚拟 DOM
    • 对比差异(Diffing
    • 提交更新到真实 DOM
  5. 浏览器渲染 :
    • 浏览器执行重绘(Repaint)显示新计数

解决方案

我们该如何解决这个问题?让它点击按钮按我们预想的count+3 呢? 看看下面的代码。

const [count, setCount] = useState(0);
const handleClick = () => {
   setCount(prev => prev + 1);
   setCount(prev => prev + 1);
   setCount(prev => prev + 1);
}
 return (
    <>
      <p>当前记数{count}</p>
      <button onClick={handleClick}>+3</button>
    </>
  )

setCount中使用一个函数,用prev参数的累加来代替count的更新,最后点击按钮一次,count便会更新为3。

但你一定有一个疑问:prev明明是不同函数调用的形参,为什么能累加到3,不应该互不相关吗?

解释

虽然 prev 是不同调用的形参,但 React 的函数式更新机制会保证它们按顺序处理并累加。具体原理如下:

  1. 更新队列机制 :

    • React 会将三个 setCount(prev => prev + 1) 放入更新队列
    • 每个更新函数会接收前一个更新计算后的最新值
  2. 执行顺序 :

    初始值: count = 0

    第一次: prev => 0 + 1 = 1

    第二次: prev => 1 + 1 = 2

    第三次: prev => 2 + 1 = 3

  3. 批量更新 :

    • React 会合并这三个更新为一次重新渲染
    • 最终 count 会直接更新为 3 关键点:
  • 函数式更新中的 prev 参数代表的是当前最新的待提交状态值
  • 不同于普通更新的闭包陷阱,函数式更新能获取到最新中间值
  • 虽然形参名相同,但每次调用时传入的 prev 值不同

总结

React 的函数式更新通过维护更新队列和传递中间值,确保了状态更新的准确性。这种机制既解决了闭包问题,又保持了高性能的批量更新特性。