设置状态变量将使另一个渲染排队。但有时你可能希望在排队下一次渲染之前对值执行多个操作。为此,有助于了解 React 如何批处理状态更新。
¥Setting a state variable will queue another render. But sometimes you might want to perform multiple operations on the value before queueing the next render. To do this, it helps to understand how React batches state updates.
你将学习到
-
”批处理” 是什么以及 React 如何使用它来处理多个状态更新
¥What “batching” is and how React uses it to process multiple state updates
-
如何连续对同一个状态变量应用多个更新
¥How to apply several updates to the same state variable in a row
React 批量状态更新
¥React batches state updates
你可能希望单击 “+3” 按钮会使计数器递增 3 次,因为它调用了 3 次 setNumber(number + 1)
:
¥You might expect that clicking the “+3” button will increment the counter three times because it calls setNumber(number + 1)
three times:
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)
多少次:
¥However, as you might recall from the previous section, each render’s state values are fixed, so the value of number
inside the first render’s event handler is always 0
, no matter how many times you call setNumber(1)
:
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
但这里还有另一个因素在起作用。在处理状态更新之前,React 会等到事件处理程序中的所有代码都已运行。这就是为什么重新渲染只发生在所有这些 setNumber()
调用之后。
¥But there is one other factor at play here. React waits until all code in the event handlers has run before processing your state updates. This is why the re-render only happens after all these setNumber()
calls.
这可能会让你想起在餐厅点菜的服务员。服务员不会一提到你的第一道菜就跑到厨房!而是,他们让你完成订单,让你对其进行更改,甚至接受桌上其他人的订单。
¥This might remind you of a waiter taking an order at the restaurant. A waiter doesn’t run to the kitchen at the mention of your first dish! Instead, they let you finish your order, let you make changes to it, and even take orders from other people at the table.
Illustrated by Rachel Lee Nabors
这使你可以更新多个状态变量 - 甚至来自多个组件 - 而不会触发太多 重新渲染。 但这也意味着在事件处理程序及其中的任何代码完成之前,UI 不会更新。这种行为(也称为批处理)使你的 React 应用运行得更快。它还避免处理只有部分变量已更新的令人困惑的 “half-finished” 渲染。
¥This lets you update multiple state variables—even from multiple components—without triggering too many re-renders. But this also means that the UI won’t be updated until after your event handler, and any code in it, completes. This behavior, also known as batching, makes your React app run much faster. It also avoids dealing with confusing “half-finished” renders where only some of the variables have been updated.
React 不会批量处理多个有意事件(例如点击) - 每次点击都是单独处理的。请放心,React 仅在通常安全的情况下才进行批处理。这确保了,例如,如果第一次单击按钮禁用了表单,则第二次单击不会再次提交它。
¥React does not batch across multiple intentional events like clicks—each click is handled separately. Rest assured that React only does batching when it’s generally safe to do. This ensures that, for example, if the first button click disables a form, the second click would not submit it again.
在下一次渲染之前多次更新相同的状态
¥Updating the same state multiple times before the next render
这是一个不常见的用例,但是如果你想在下一次渲染之前多次更新同一个状态变量,而不是像 setNumber(number + 1)
那样传递下一个状态值,你可以传递一个根据前一个状态计算下一个状态的函数到队列中,例如 setNumber(n => n + 1)
。这是一种告诉 React “用状态值做某事” 而不是仅仅替换它的方法。
¥It is an uncommon use case, but if you would like to update the same state variable multiple times before the next render, instead of passing the next state value like setNumber(number + 1)
, you can pass a function that calculates the next state based on the previous one in the queue, like setNumber(n => n + 1)
. It is a way to tell React to “do something with the state value” instead of just replacing it.
现在尝试增加计数器:
¥Try incrementing the counter now:
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
被称为更新器函数。当你将其传递给状态设置器时:
¥Here, n => n + 1
is called an updater function. When you pass it to a state setter:
-
在事件处理程序中的所有其他代码运行之后,React 将此函数排队等待处理。
¥React queues this function to be processed after all the other code in the event handler has run.
-
在下一次渲染期间,React 遍历队列并为你提供最终的更新状态。
¥During the next render, React goes through the queue and gives you the final updated state.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
以下是 React 在执行事件处理程序时如何处理这些代码行:
¥Here’s how React works through these lines of code while executing the event handler:
-
setNumber(n => n + 1)
:n => n + 1
是一个函数。React 将其添加到队列中。¥
setNumber(n => n + 1)
:n => n + 1
is a function. React adds it to a queue. -
setNumber(n => n + 1)
:n => n + 1
是一个函数。React 将其添加到队列中。¥
setNumber(n => n + 1)
:n => n + 1
is a function. React adds it to a queue. -
setNumber(n => n + 1)
:n => n + 1
是一个函数。React 将其添加到队列中。¥
setNumber(n => n + 1)
:n => n + 1
is a function. React adds it to a queue.
当你在下一次渲染期间调用 useState
时,React 会遍历队列。之前的 number
状态是 0
,所以这就是 React 作为 n
参数传递给第一个更新函数的状态。然后 React 将你之前的更新函数的返回值作为 n
传递给下一个 updater,依此类推:
¥When you call useState
during the next render, React goes through the queue. The previous number
state was 0
, so that’s what React passes to the first updater function as the n
argument. Then React takes the return value of your previous updater function and passes it to the next updater as n
, and so on:
排队更新 | 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
返回它。
¥React stores 3
as the final result and returns it from useState
.
这就是为什么在上例中单击 “+3” 会正确地将值增加 3。
¥This is why clicking “+3” in the above example correctly increments the value by 3.
如果在替换状态后更新状态会发生什么
¥What happens if you update state after replacing it
这个事件处理程序怎么样?你认为 number
会在下一次渲染中出现什么?
¥What about this event handler? What do you think number
will be in the next render?
<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 执行的操作:
¥Here’s what this event handler tells React to do:
-
setNumber(number + 5)
:number
是0
,所以setNumber(0 + 5)
。React 将“替换为5
”添加到其队列中。¥
setNumber(number + 5)
:number
is0
, sosetNumber(0 + 5)
. React adds “replace with5
” to its queue. -
setNumber(n => n + 1)
:n => n + 1
是一个更新函数。React 将该函数添加到它的队列中。¥
setNumber(n => n + 1)
:n => n + 1
is an updater function. React adds that function to its queue.
在下一次渲染期间,React 遍历状态队列:
¥During the next render, React goes through the state queue:
排队更新 | n | 返回 |
---|---|---|
“替换为 5 ” | 0 (未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React 将 6
存储为最终结果并从 useState
返回它。
¥React stores 6
as the final result and returns it from useState
.
如果在更新状态后替换它会发生什么
¥What happens if you replace state after updating it
让我们再试一个例子。你认为 number
会在下一次渲染中出现什么?
¥Let’s try one more example. What do you think number
will be in the next render?
<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 在执行此事件处理程序时如何处理这些代码行:
¥Here’s how React works through these lines of code while executing this event handler:
-
setNumber(number + 5)
:number
是0
,所以setNumber(0 + 5)
。React 将“替换为5
”添加到其队列中。¥
setNumber(number + 5)
:number
is0
, sosetNumber(0 + 5)
. React adds “replace with5
” to its queue. -
setNumber(n => n + 1)
:n => n + 1
是一个更新函数。React 将该函数添加到它的队列中。¥
setNumber(n => n + 1)
:n => n + 1
is an updater function. React adds that function to its queue. -
setNumber(42)
:React 将“替换为42
”添加到其队列中。¥
setNumber(42)
: React adds “replace with42
” to its queue.
在下一次渲染期间,React 遍历状态队列:
¥During the next render, React goes through the state queue:
排队更新 | n | 返回 |
---|---|---|
“替换为 5 ” | 0 (未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
“替换为 42 ” | 6 (未使用) | 42 |
然后 React 将 42
存储为最终结果并从 useState
返回它。
¥Then React stores 42
as the final result and returns it from useState
.
总而言之,你可以通过以下方式考虑传递给 setNumber
状态设置器的内容:
¥To summarize, here’s how you can think of what you’re passing to the setNumber
state setter:
-
更新器函数(例如
n => n + 1
)被添加到队列中。¥An updater function (e.g.
n => n + 1
) gets added to the queue. -
任何其他值(例如数字
5
)都会将“替换为5
”添加到队列中,忽略已排队的内容。¥Any other value (e.g. number
5
) adds “replace with5
” to the queue, ignoring what’s already queued.
事件处理程序完成后,React 将触发重新渲染。在重新渲染期间,React 将处理队列。更新器函数在渲染期间运行,因此更新器函数必须是 纯粹的 并且仅返回结果。不要尝试从它们内部设置状态或运行其他副作用。在严格模式下,React 将运行每个更新程序函数两次(但丢弃第二次结果)以帮助你发现错误。
¥After the event handler completes, React will trigger a re-render. During the re-render, React will process the queue. Updater functions run during rendering, so updater functions must be pure and only return the result. Don’t try to set state from inside of them or run other side effects. In Strict Mode, React will run each updater function twice (but discard the second result) to help you find mistakes.
命名约定
¥Naming conventions
通常用相应状态变量的首字母命名更新函数参数:
¥It’s common to name the updater function argument by the first letters of the corresponding state variable:
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
如果你喜欢更冗长的代码,另一个常见的约定是重复完整的状态变量名称,如 setEnabled(enabled => !enabled)
,或使用像 setEnabled(prevEnabled => !prevEnabled)
这样的前缀。
¥If you prefer more verbose code, another common convention is to repeat the full state variable name, like setEnabled(enabled => !enabled)
, or to use a prefix like setEnabled(prevEnabled => !prevEnabled)
.
回顾
-
设置状态不会更改现有渲染中的变量,但它会请求一个新的渲染。
¥Setting state does not change the variable in the existing render, but it requests a new render.
-
React 在事件处理程序完成运行后处理状态更新。这称为批处理。
¥React processes state updates after event handlers have finished running. This is called batching.
-
要在一个事件中多次更新某个状态,可以使用
setNumber(n => n + 1)
更新函数。¥To update some state multiple times in one event, you can use
setNumber(n => n + 1)
updater function.
挑战 1 / 2: 修复请求计数器
¥Fix a request counter
你正在开发一个艺术品市场应用,该应用允许用户同时为一件艺术品提交多个订单。每次用户按下 “购买” 按钮时,“待办的” 计数器应增加 1。三秒后,“待办的” 计数器应该减少,“完成的” 计数器应该增加。
¥You’re working on an art marketplace app that lets the user submit multiple orders for an art item at the same time. Each time the user presses the “Buy” button, the “Pending” counter should increase by one. After three seconds, the “Pending” counter should decrease, and the “Completed” counter should increase.
但是,“待办的” 计数器不按预期运行。当你按下 “购买” 时,它会减少到 -1
(这应该是不可能的!)。如果你快速单击两次,两个计数器的行为似乎都无法预测。
¥However, the “Pending” counter does not behave as intended. When you press “Buy”, it decreases to -1
(which should not be possible!). And if you click fast twice, both counters seem to behave unpredictably.
为什么会这样?修复两个计数器。
¥Why does this happen? Fix both counters.
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); }); }