React + TypeScript: 在队列中添加并处理连续的状态变化
React官方网站的文档已于2023年3月16日进行了修订(请参考「Introducing react.dev」)。本文是对基本解释中「Queueing a Series of State Updates」的简要总结。但是,在代码中添加了TypeScript。与此同时,我们省略了面向初学者的JavaScript基础解释。
此外,有关本系列解说的其他文章,请参考”React + TypeScript:学习React官方文档的基本解说《学习React》”。
设置状态变量后,渲染将进入队列。但在下一个渲染加入队列之前,可能有时我们需要在变量值上进行多个操作。为了实现这一点,了解React如何批处理状态更新将会很有帮助。
用React进行批量处理的状态更新。
例如,当点击[+3]按钮时,计数器的状态变量(number)的值会增加多少呢?您可以通过以下的Sample 001的CodeSandbox代码来确认结果。
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>
</>
);
}
React + TypeScript:按顺序排列一系列状态更新的队列01。
每次点击按钮时,即使调用设置函数setNumber(number + 1)三次,变量值也只会递增1。这是因为状态变量值在每次渲染时都是固定的(参见“渲染是当前快照”)。换句话说,当首次点击按钮并渲染时,无论在事件处理程序内调用设置函数(setNumber())多少次进行累加,目标状态变量(number)的值仍然是0。
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
在这里还有一件需要注意的事情。React会等待在事件处理程序中的代码全部执行完之后再更新状态。因此,在所有setNumber()的调用都结束之后,才会进行重新渲染。
在餐厅里,服务员接订单可能有点像。服务员不会每次点一道菜都要跑进厨房。他们应该会先听一遍桌上的订单。在这期间,他们还可以接受更改菜品和同桌其他人的订单。
这样一来,就避免了不必要的重新渲染,可以更新多个状态变量。即使有多个组件也是一样的。这也意味着在处理事件处理程序和其中的代码之前,UI不会更新。这个称为”批处理”的功能大大加快了React应用程序的运行速度。此外,还可以避免一些变量保持”不完整”的状态而进行渲染。
然而,React不会批处理意图明确的事件,例如点击事件。它会将每个点击事件单独处理。React只在一般情况下认为批处理是安全的。比如,如果通过点击按钮禁用表单,在下一个点击中它不会被提交。
渲染前多次更新相同的状态变量。
虽然很少使用,但在进行渲染之前,不是不能多次更新相同的状态变量。下一个状态变量的值不是通过将参数值写成setNumber(number + 1)这样的设置函数来替换,而是作为计算下一个状态的回调函数传递(参见“函数式更新”)。例如,使用setNumber((n) => n + 1),下一个值将根据队列中前一个状态进行计算。React规定我们应该如何处理状态值,而不是直接传递修改后的值。请在下面的Sample002中确认两者之间的差异。
<button
onClick={() => {
setNumber((n) => n + 1);
setNumber((n) => n + 1);
setNumber((n) => n + 1);
}}
>
+3
</button>
样例002 ■React + TypeScript:排队一系列的状态更新02
将传递给设置函数(setNumber())的参数的回调称为“更新函数”。
-
- React将更新函数放入队列中,在事件处理代码全部执行完毕后调用它来处理。
- 在下一次渲染时,React会处理队列,并返回最终更新后的状态。
在下一次渲染时调用的useState,React按顺序遍历队列。上一次的状态变量number的值为0。React将该值作为参数n传递给初始更新函数。然后,React将更新后的返回值作为参数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。
在更新状态变量值之后进行更新的情况下。
请问在修改状态变量值之后,如果在更新函数中对该值进行处理,会发生什么情况呢?请通过以下的样例003进行确认。
<button
onClick={() => {
setNumber(number + 5);
setNumber((n) => n + 1);
}}
>
Increase the number
</button>
样本003■React + TypeScript: 队列一系列的状态更新03
事件处理程序在React中被告知如下。
-
- setNumber(number + 5): 上一次的狀態變數number的值為0。React將「改寫」成 0 + 5 = 5,並將其放入佇列中。
setNumber((n) => n + 1): (n) => n + 1 是更新函數。React將此「更新函數」放入佇列中。
5
への書き替え」0
(使用せず)5
(n) => n + 1
5
5 + 1 = 6
在下一次渲染中,React将按顺序处理队列。然后,将6作为最终值保存并从useState返回。可以将值的重新赋值视为传递更新函数((n)=>x),其中n参数未使用。
在更新状态变量值后进行了重写的情况下
更新函数处理完状态变量值之后,如果进一步更改了值,会怎么样呢?setNumber()函数的前两个调用与前述代码示例相同。然后,我们添加了setNumber(42)。
<button
onClick={() => {
setNumber(number + 5);
setNumber((n) => n + 1);
setNumber(42);
}}
>
Increase the number
</button>
样品004■React + TypeScript:排队一系列状态更新04
第三个设置函数的调用会将状态变量的值改为42。React会将42作为最终值保留,并从useState返回。需要注意的是,将第三个设置函数设置为setNumber(number + 42)也不会改变结果。这是因为状态变量number的值被固定为上一次的0。传递给第二个设置函数的更新函数的返回值6不会被使用。
5
への書き替え」0
(使用せず)5
(n) => n + 1
5
5 + 1 = 6
「42
への書き替え」6
(使用せず)42
让我们总结一下当将更新函数和替换值分别传递给setNumber()函数时的情况。
更新関数(例: (n) => n + 1): キューには更新関数が加えられて値を処理する。
値の書き替え(例: number = 5): キューに「5への書き替え」が加えられ、それまでのキューで処理した値は上書きされる。
当事件处理代码执行完毕后,React会开始重新渲染。在重新渲染时,React会处理队列操作。更新函数会在渲染期间执行,所以它必须是一个纯函数,只返回结果,不能在其中设置状态或执行其他副作用。
在开发时启用StrictMode的情况下,React会对每个更新函数进行两次调用(但第二次的结果会被丢弃),这是为了更容易发现代码问题(参见”React + TypeScript: React 18中组件挂载时useEffect被调用两次”)。
取名规则
一般而言,更新函数的参数名称常常使用状态变量的首字母。
setEnabled((e) => !e);
setLastName((ln) => ln.reverse());
setFriendCount((fc) => fc * 2);
当你想要更详细地编写代码时,可以直接使用状态变量名称,例如setEnabled((enabled) => !enabled),也可能会加上前缀,例如setEnabled((prevEnabled) => !prevEnabled)。
总结
在这篇文章中,我解释了以下项目。
-
- 状態を設定しても、既存のレンダリングの変数は変わりません。新たなレンダリングが求められます。
-
- Reactはイベントハンドラの実行が終わったあと、状態の更新を処理します。これがバッチ処理です。
- 1回のイベントで状態を複数回更新するには、setNumber((n) => n + 1)といった更新関数を用います。