对一系列状态更新进行排队
设置状态变量将使另一个渲染排队。 但有时您可能希望在排队下一次渲染之前对值执行多个操作。 为此,有助于了解 React 如何批处理状态更新。
你将学习
- 什么是“ batching”以及 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>
</>
)
}
然而,你可能还记得上一节中,每个渲染器的状态值都是固定的,所以第一个渲染器的事件处理程序中的数字值始终为 0,无论你调用 setNumber(1) 多少次:
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
但这里还有另一个因素需要讨论。 在处理状态更新之前,React 会等到事件处理程序中的所有代码都已运行。 这就是为什么重新渲染只发生在所有这些 setNumber() 调用之后。
这可能会让您想起在餐厅点菜的服务员。 服务员不会一提到您的第一道菜就跑到厨房! 相反,他们让您完成订单,让您对其进行更改,甚至接受桌上其他人的订单。
这使您可以更新多个状态变量——甚至来自多个组件——而不会触发太多重新渲染。 但这也意味着在您的事件处理程序及其中的任何代码完成之前,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 会遍历队列。 之前的数字状态是 0,因此 React 将其作为 n 参数传递给第一个更新函数。 然后 React 将你之前的 updater 函数的返回值作为 n 传递给下一个 updater,依此类推:
排队更新 | 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。
如果在替换状态后更新状态会发生什么
这个事件处理程序怎么样? 您认为下一次渲染中的数字是多少?
<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 | 返回值 |
---|---|---|
“replace with 5” | 0(未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React 存储 6 作为最终结果并从 useState 返回它。
你可能已经注意到 setState(x) 实际上像 setState(n => x) 一样工作,但是 n 没有被使用!
如果在更新状态后替换状态会发生什么
让我们再试一个例子。 您认为下一次渲染中的数字是多少?
<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 | 返回值 |
---|---|---|
“replace with 5” | 0(未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
“replace with 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) updater 函数。
网友评论