设置状态变量将排队进行另一次渲染。但有时你可能希望在排队进行下一次渲染之前对该值执行多个操作。为此,了解 React 如何对状态更新进行批处理会有所帮助。
你将学习
- 什么是“批处理”以及 React 如何使用它来处理多个状态更新
- 如何连续对同一个状态变量应用多个更新
React 对状态更新进行批处理
你可能认为点击“+3”按钮会使计数器递增三次,因为它调用了 setNumber(number + 1)
三次
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> </> ) }
但是,你可能还记得上一节的内容,每个渲染的状态值都是固定的,因此第一次渲染的事件处理程序中 number
的值始终为 0
,无论你调用 setNumber(1)
多少次
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
但这里还有另一个因素在起作用。 React 会等到事件处理程序中的 *所有* 代码都运行完毕后,才会处理你的状态更新。 这就是为什么重新渲染只在所有这些 setNumber()
调用 *之后* 发生的原因。
这可能会让你想起餐厅服务员下单的过程。服务员不会在你提到你的第一道菜时就跑到厨房去!相反,他们会让你点完菜,让你修改订单,甚至接受同一桌其他人的点菜。
插图作者 Rachel Lee Nabors
这允许你更新多个状态变量(即使来自多个组件),而不会触发过多的 重新渲染。 但这也意味着在你的事件处理程序及其中的所有代码都完成 *之后*,UI 才会更新。这种行为,也称为 批处理, 可以使你的 React 应用运行得更快。它还可以避免处理令人困惑的“半成品”渲染,在这种渲染中,只有一部分变量被更新了。
React 不会跨 *多个* 有意事件(如点击)进行批处理——每个点击都会被单独处理。请放心,React 只会在通常安全的情况下进行批处理。例如,这确保了如果第一次按钮点击禁用了表单,则第二次点击不会再次提交它。
在下一次渲染之前多次更新同一个状态
这是一种不常见的用例,但如果你希望在下一次渲染之前多次更新同一个状态变量,而不是像 setNumber(number + 1)
那样传递 *下一个状态值*,你可以传递一个 *函数*,该函数根据队列中的前一个状态计算下一个状态,例如 setNumber(n => n + 1)
。这是一种告诉 React“对状态值执行某些操作”而不是仅仅替换它的方法。
现在尝试递增计数器
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); }}>+3</button> </> ) }
这里,n => n + 1
被称为 更新器函数。 当你将它传递给状态设置器时
- React 会将此函数排队,以便在事件处理程序中的所有其他代码都运行完毕后进行处理。
- 在下一次渲染期间,React 会遍历队列并为你提供最终更新的状态。
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
以下是 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
将是什么?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); }}>Increase the number</button> </> ) }
以下是此事件处理程序告诉 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
返回。
如果您在更新状态后替换它,会发生什么
让我们再试一个例子。您认为在下一次渲染中 number
将是什么?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); setNumber(42); }}>Increase the number</button> </> ) }
以下是 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(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
如果您更喜欢更详细的代码,另一个常见的约定是重复完整的状变量名称,例如 setEnabled(enabled => !enabled)
,或者使用前缀,例如 setEnabled(prevEnabled => !prevEnabled)
。
总结
- 设置状态不会更改现有渲染中的变量,但会请求新的渲染。
- React 在事件处理程序运行完毕后处理状态更新。这称为批处理。
- 要在一次事件中多次更新某个状态,可以使用
setNumber(n => n + 1)
更新器函数。
挑战 1的 2: 修复请求计数器
您正在开发一个艺术品交易平台应用程序,允许用户同时提交多个艺术品订单。每次用户按下“购买”按钮时,“待处理”计数器应增加 1。三秒钟后,“待处理”计数器应减少,“已完成”计数器应增加。
但是,“待处理”计数器的行为不符合预期。当你按下“购买”时,它会减少到 -1
(这不应该发生!)。如果你快速点击两次,两个计数器的行为似乎都不可预测。
为什么会发生这种情况?修复这两个计数器。
import { useState } from 'react'; export default function RequestTracker() { const [pending, setPending] = useState(0); const [completed, setCompleted] = useState(0); async function handleClick() { setPending(pending + 1); await delay(3000); setPending(pending - 1); setCompleted(completed + 1); } return ( <> <h3> Pending: {pending} </h3> <h3> Completed: {completed} </h3> <button onClick={handleClick}> Buy </button> </> ); } function delay(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }); }