状态更新队列
设置状态变量会触发另一次渲染。但有时你可能希望在触发下一次渲染之前对值执行多个操作。为此,了解 React 如何批量处理状态更新会有所帮助。
你将学习
- 什么是“批处理”以及 React 如何使用它来处理多个状态更新
- 如何连续对同一状态变量应用多次更新
React 批量处理状态更新
你可能会认为点击“+3”按钮会使计数器增加三次,因为它调用了三次setNumber(number + 1):
然而,正如你可能从上一节回忆起的,每次渲染的状态值是固定的,因此第一次渲染的事件处理程序中的number值始终是0,无论你调用多少次setNumber(1):
但这里还有另一个因素在起作用。React 会等到事件处理程序中的所有代码运行完毕后,才处理你的状态更新。这就是为什么重新渲染只会在所有这些之后发生。setNumber()调用
这可能会让你想起餐厅服务员点餐的场景。服务员不会在你提到第一道菜时就跑向厨房!相反,他们会让你完成点餐,允许你修改订单,甚至接受同桌其他人的点餐。

插图作者:Rachel Lee Nabors
这让你可以更新多个状态变量——甚至来自多个组件——而不会触发过多的重新渲染。但这也意味着 UI 在你的事件处理程序及其内部任何代码完成之后才会更新。这种行为,也称为批处理,使你的 React 应用运行得更快。它还避免了处理令人困惑的“半成品”渲染,即只有部分变量被更新的情况。
React 不会跨多个有意识的事件(如点击)进行批处理——每次点击都是单独处理的。请放心,React 只会在通常安全的情况下进行批处理。这确保了,例如,如果第一次点击禁用了表单,第二次点击不会再次提交它。
在下一次渲染前多次更新同一状态
这是一个不常见的用例,但如果你想在下一次渲染之前多次更新同一状态变量,与其传递下一个状态值,例如setNumber(number + 1),你可以传递一个函数,该函数根据队列中的前一个状态计算下一个状态,例如setNumber(n => n + 1)。这是一种告诉 React“对状态值执行某些操作”而不仅仅是替换它的方式。
现在尝试增加计数器:
在这里,n => n + 1被称为更新函数。当你将其传递给状态设置函数时:
- React 将此函数排队,以便在事件处理程序中的所有其他代码运行后处理。
- 在下一次渲染期间,React 遍历队列并给你最终更新后的状态。
以下是 React 在执行事件处理程序时处理这些代码行的方式:
setNumber(n => n + 1):n => n + 1是一个函数。React 将其加入队列。setNumber(n => n + 1):n => n + 1是一个函数。React 将其加入队列。setNumber(n => n + 1):n => n + 1是一个函数。React 将其加入队列。
在下一次渲染期间调用useState 时,React 会遍历队列。上一次的 number 状态是 0,因此 React 将其作为 n参数传递给第一个更新器函数。然后 React 获取上一个更新器函数的返回值,并将其作为n传递给下一个更新器,依此类推:
| 队列中的更新 | n | 返回值 |
|---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
React 将3 存储为最终结果,并从 useState返回。
这就是为什么在上面的示例中点击“+3”会将值正确递增 3。
如果在替换状态后更新状态会发生什么
那么这个事件处理函数呢?你认为下一次渲染时number 会是多少?
以下是这个事件处理函数告诉 React 要做的事情:
setNumber(number + 5):number是0,所以setNumber(0 + 5)。React 将“替换为5”加入其队列。setNumber(n => n + 1):n => n + 1是一个更新器函数。React 将该函数加入其队列。
在下一次渲染期间,React 会遍历状态队列:
| 已排队的更新 | n | 返回值 |
|---|---|---|
“替换为 5” | 0(未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React 将6 存储为最终结果,并从 useState返回它。
注意
你可能已经注意到,setState(5) 实际上类似于 setState(n => 5),但n未被使用!
如果在更新状态后替换状态会发生什么
让我们再试一个例子。你认为下一次渲染时number 会是多少?
以下是 React 在执行此事件处理程序时处理这些代码行的过程:
setNumber(number + 5):number是0,所以setNumber(0 + 5)。React 将“替换为5”添加到其队列中。setNumber(n => n + 1):n => n + 1是一个更新函数。React 将该函数添加到其队列中。setNumber(42):React 将“替换为42”添加到其队列中。
在下次渲染期间,React 会遍历状态队列:
| 已排队的更新 | n | 返回值 |
|---|---|---|
“替换为 5” | 0(未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
“替换为 42” | 6(未使用) | 42 |
然后 React 将42 存储为最终结果,并从 useState返回它。
总结一下,你可以这样理解传递给 setNumber状态设置函数的内容:
- 一个更新器函数(例如
n => n + 1)会被添加到队列中。 - 任何其他值(例如数字
5)会添加“替换为5”到队列中,忽略已排队的内容。
事件处理函数完成后,React 将触发重新渲染。在重新渲染期间,React 会处理队列。更新器函数在渲染期间运行,因此更新器函数必须是纯函数,并且只返回结果。不要尝试在它们内部设置状态或运行其他副作用。在严格模式下,React 会运行每个更新器函数两次(但丢弃第二次结果)以帮助你发现错误。
命名约定
通常使用对应状态变量名称的首字母来命名更新器函数的参数:
如果你更喜欢更详细的代码,另一种常见的约定是重复完整的状态变量名,例如setEnabled(enabled => !enabled),或者使用前缀,例如setEnabled(prevEnabled => !prevEnabled)。
回顾
- 设置状态不会改变现有渲染中的变量,而是请求一次新的渲染。
- React 在事件处理函数运行完毕后处理状态更新。这称为批处理。
- 要在单个事件中多次更新某个状态,你可以使用
setNumber(n => n + 1)更新器函数。
尝试一些挑战
Challenge 1 of 2:修复请求计数器 #
你正在开发一个艺术品市场应用,允许用户同时为一件艺术品提交多个订单。每次用户按下“购买”按钮时,“待处理”计数器应增加一。三秒后,“待处理”计数器应减少,而“已完成”计数器应增加。
然而,“待处理”计数器的行为不符合预期。当你按下“购买”时,它会减少到 -1(这应该是不可能的!)。如果你快速点击两次,两个计数器的行为似乎都变得不可预测。
为什么会发生这种情况?修复这两个计数器。
