问题现象
在 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 的执行流程如下(以当前代码为例):
- 事件触发 :点击按钮调用
handleClick函数 - 批量更新阶段 :
React将三个setCount(count + 1)放入更新队列(此时count仍是初始值 0)- 由于是同步代码,
React会合并这些更新(性能优化)
- 调度阶段 :
React将状态更新标记为待处理- 安排重新渲染(通过
React的调度机制)
- 渲染阶段 :
React计算最终状态:由于三次setCount都基于相同初始值,最终只执行一次count + 1- 生成新的虚拟
DOM - 对比差异(
Diffing) - 提交更新到真实
DOM
- 浏览器渲染 :
- 浏览器执行重绘(
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 的函数式更新机制会保证它们按顺序处理并累加。具体原理如下:
-
更新队列机制 :
React会将三个setCount(prev => prev + 1)放入更新队列- 每个更新函数会接收前一个更新计算后的最新值
-
执行顺序 :
初始值:
count = 0第一次:
prev => 0 + 1 = 1第二次:
prev => 1 + 1 = 2第三次:
prev => 2 + 1 = 3 -
批量更新 :
React会合并这三个更新为一次重新渲染- 最终
count会直接更新为3关键点:
- 函数式更新中的
prev参数代表的是当前最新的待提交状态值 - 不同于普通更新的闭包陷阱,函数式更新能获取到最新中间值
- 虽然形参名相同,但每次调用时传入的
prev值不同
总结
React 的函数式更新通过维护更新队列和传递中间值,确保了状态更新的准确性。这种机制既解决了闭包问题,又保持了高性能的批量更新特性。