前言
之前作者写过一篇文章,初步介绍了在React中,是如何进行渲染,这篇文章讲讲渲染前要做的批处理和更新队列。
"在 React 中,状态更新不是命令式的立即执行,而是声明式的加入队列。" - React 核心原则
从一道题开始
观察下列代码,你认为点击button时h1中的number会变成几?
js
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
我们发现,即使在onClick
事件处理函数中setNumber(number+1)
三次,但是我们每次点击"+3"按钮后,数字会只增加1。

那么这到底是为什么呢,让我接下来为你揭秘其背后的原理!
React 的批处理机制
什么是批处理?
React 不会在每次 setState
调用后立即更新组件并重新渲染。相反,它会将同一事件处理函数中的所有状态更新收集起来放入队列 ,批量处理后再一次性更新 UI(也就是渲染) 。这种优化策略称为批处理(batching) 。
比如上面的例子
-
第一次setNumber(number + 1); 此时不会渲染,会将该状态更新加入队列
-
第二次setNumber(number + 1); 此时不会渲染,会将该状态更新加入队列
-
第三次setNumber(number + 1); 此时不会渲染,会将该状态更新加入队列
-
当
onClick
整个事件处理函数 完成之后,此时它才会批量处理队列中的刚刚添加的所有状态更新 -
处理完状态更新之后,它此时会重渲染,从而我们能看到页面中的0变成了1
而至于为什么是变成1而不是3,其实是闭包的原因!
闭包如何捕获状态值
首先我们要明确我们React在初始会进行初次渲染,此时是基于state是为0的环境下渲染的
所以在整个onClick事件处理函数开始时,number初始为0,我们在页面上看到的也是0
js
const [number, setNumber] = useState(0);
在JavaScript中,闭包会记住创建它时的外部变量值。在事件处理函数onClick
被创建时,它捕获了当前的number
值(0)。在事件触发时,虽然状态可能已经发生了变化,但在这个闭包中,number
仍然是创建时的值(0)。因此,多次调用setNumber(number + 1)
都使用0作为基础值。
所以上面的过程实际上是:
- 所有三个
setNumber
调用使用的number
都是同一个闭包捕获的值 (初始值 0),所以刚刚提到的将状态加入队列:在这里是将setNumber(0 + 1);
加入队列 - 执行完事件处理函数
onClick
后,在队列中处理状态更新,连续执行了三次setNumber(1)
- 此时我们重渲染,就将基于state=1的结果渲染,导致我们在页面上看到内容为1
图形演示:
如何解决闭包的问题呢
我们接下来就要解决闭包的带来的问题,使得在点击按钮"+3"时,能够如愿地让0变成3。
解决方式是使用函数式更新: 将onClick改为如下:
js
// 由原来的 setNumber(number + 1)改为了setNumber(n => n + 1);
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
我们能看到,将值传递,改为传递更新函数(n => n+1) 就能解决问题。
这是为什么呢?
这里的批处理过程仍然和上面的是一样,
- 每次setNumber(n => n + 1)时不会重渲染页面,而是将这个状态更新加入到队列中。
- 当整个事件处理函数
onClick
完成之后,再按顺序处理现在队列 - 队列里是刚刚加入的三个
setNumber(n => n + 1)
,按顺序执行
只不过这里不会受闭包的影响:
(n => n+1)
该函数每次更新都基于前一次更新后的状态,而不是一直记住初始的number。
-
第一次:
prevState => prevState + 1
-> 0+1=1 -
第二次:
prevState => prevState + 1
-> 1+1=2 -
第三次:
prevState => prevState + 1
-> 2+1=3
函数式更新不依赖于闭包中捕获的状态变量,而是依赖于React内部维护的更新队列。React会保证在应用更新时,每个更新函数都能接收到最新的、已经过之前更新修改的状态。
它的过程是这样的:
需要注意
在函数式更新时,在每次将状态更新加入队列的过程中,n
的值此时并未改变 。只有在事件处理函数完全执行完毕后,React 才会批量处理队列中的更新函数 ,此时 n
的值才会改变。
区别
最后总结一下,函数式更新和直接更新的区别:
特性 | 直接更新 (setNumber(number+1) ) |
函数式更新 (setNumber(n=>n+1) ) |
---|---|---|
使用的状态值 | 当前渲染周期的固定值 (闭包捕获) | React 内部待处理的最新状态 |
更新依据 | 基于渲染时的状态快照 | 基于前一个更新计算的结果 |
多次更新结果 | 会被合并覆盖 (+1) | 形成更新链 (+3) |
闭包依赖 | 是 (使用外部变量) | 否 (使用参数 n) |
批处理的混合场景分析
好了,通过上面的分析,你应该能够知道批处理的整个流程以及其中的规则。
下面给出几个复杂场景的应用,根据上面的规则,你应该能够分析出来这几个场景的结果:
4.1 场景一:先替换后更新
js
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
执行过程:
setNumber(number + 5)
→ 加入队列:"替换为 5"setNumber(n => n + 1)
→ 加入队列:更新函数
根据刚刚提到的,函数式更新会记住前面的状态,所以,更新队列处理:
更新队列 | n | 返回值 |
---|---|---|
"替换为 5" | - | 5 |
n => n + 1 | 5 | 6 |
最终结果:6
4.2 场景二:混合更新后替换
js
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
执行过程:
setNumber(number + 5)
→ "替换为 5"setNumber(n => n + 1)
→ 更新函数setNumber(42)
→ "替换为 42"
这里要注意:按队列的顺序执行,所以最后一步是直接将值替换为42,会完全覆盖当前的状态
更新队列处理:
更新队列 | n | 返回值 |
---|---|---|
"替换为 5" | - | 5 |
n => n + 1 | 5 | 6 |
"替换为 42" | - | 42 |
最终结果:42(最后一步替换了之前的所有更新)
批处理的好处
- 性能优化:减少不必要的渲染次数
- 避免"半成品"渲染:确保 UI 更新基于完整的状态变更
- 维护状态一致性:防止部分状态更新导致界面不一致
总结:掌握状态更新队列
理解 React 的状态批处理和更新队列机制是成为高级 React 开发者的关键一步:
- 批处理机制:React 会收集事件处理函数中的所有状态更新,批量处理后再更新 UI
- 闭包陷阱:直接使用状态值更新会捕获过时的状态快照
- 函数式更新:通过传递更新函数 (n => n + 1) 解决闭包问题
- 更新队列:React 按顺序执行更新队列中的函数,每个函数接收前一个更新的结果
- 混合更新:直接更新 ("替换为 X") 会重置更新队列
通过合理使用函数式更新,我们可以精确控制状态变更流程,避免常见的状态更新陷阱,构建更加健壮的 React 应用。
🌇结尾
本文部分内容:参考React官方文档
感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)
作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。