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()的调用都结束之后,才会进行重新渲染。

在餐厅里,服务员接订单可能有点像。服务员不会每次点一道菜都要跑进厨房。他们应该会先听一遍桌上的订单。在这期间,他们还可以接受更改菜品和同桌其他人的订单。

menu_tenin_yobu.png

这样一来,就避免了不必要的重新渲染,可以更新多个状态变量。即使有多个组件也是一样的。这也意味着在处理事件处理程序和其中的代码之前,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())的参数的回调称为“更新函数”。

    1. React将更新函数放入队列中,在事件处理代码全部执行完毕后调用它来处理。

 

    在下一次渲染时,React会处理队列,并返回最终更新后的状态。

在下一次渲染时调用的useState,React按顺序遍历队列。上一次的状态变量number的值为0。React将该值作为参数n传递给初始更新函数。然后,React将更新后的返回值作为参数n传递给下一个更新函数。这个过程将依次重复。

キューに入った更新n戻り値(n) => n + 100 + 1 = 1(n) => n + 111 + 1 = 2(n) => n + 122 + 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中被告知如下。

    1. setNumber(number + 5): 上一次的狀態變數number的值為0。React將「改寫」成 0 + 5 = 5,並將其放入佇列中。

setNumber((n) => n + 1): (n) => n + 1 是更新函數。React將此「更新函數」放入佇列中。

キューに入った更新n戻り値「5への書き替え」0(使用せず)5(n) => n + 155 + 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不会被使用。

キューに入った更新n戻り値「5への書き替え」0(使用せず)5(n) => n + 155 + 1 = 642への書き替え」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)といった更新関数を用います。
广告
将在 10 秒后关闭
bannerAds