队列一系列状态更新

设置状态变量将会排队执行另一次渲染。但有时你可能想在排队下一次渲染之前对该值执行多个操作。为此,理解 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 如何使用它来处理多个状态更新
  • 如何连续对同一个状态变量应用多个更新

React 批量状态更新

🌐 React batches state updates

你可能会认为点击“+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>
    </>
  )
}

然而,正如你可能还记得的上一节中提到的,每次渲染的状态值都是固定的,因此即使你多次调用 setNumber(1),第一次渲染的事件处理程序中的 number 的值始终是 0

🌐 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.

An elegant cursor at a restaurant places and order multiple times with React, playing the part of the waiter. After she calls setState() multiple times, the waiter writes down the last one she requested as her final order.

Illustrated by Rachel Lee Nabors

这让你可以更新多个状态变量——甚至来自多个组件——而不会触发过多的重新渲染。但这也意味着UI要等到你的事件处理程序及其中的任何代码完成之后才会更新。这种行为,也称为批处理,使你的React应用运行得更快。它还避免了处理令人困惑的“未完成”渲染情况,即只有部分变量已被更新。

🌐 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 只有在通常安全的情况下才会进行批处理。这确保了例如,如果第一次点击按钮禁用了表单,第二次点击不会再次提交表单。

在下一次渲染之前多次更新相同的状态

🌐 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:

  1. 在事件处理程序中的所有其他代码运行之后,React 将此函数排队等待处理。
  2. 在下一次渲染期间,React 遍历队列并为你提供最终的更新状态。
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:

  1. setNumber(n => n + 1)n => n + 1 是一个函数。React 将其添加到队列中。
  2. setNumber(n => n + 1)n => n + 1 是一个函数。React 将其添加到队列中。
  3. setNumber(n => n + 1)n => n + 1 是一个函数。React 将其添加到队列中。

当你在下一次渲染中调用 useState 时,React 会遍历队列。之前的 number 状态是 0,所以 React 将其作为 n 参数传递给第一个更新函数。然后 React 会将你之前更新函数的返回值传递给下一个更新函数作为 n,以此类推:

🌐 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 + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 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:

  1. setNumber(number + 5)number0,所以 setNumber(0 + 5)。React 将 “替换为 5 添加到它的队列中。
  2. setNumber(n => n + 1)n => n + 1 是一个更新函数。React 会将该函数加入它的队列。

在下一次渲染期间,React 遍历状态队列:

🌐 During the next render, React goes through the state queue:

排队更新n返回值
“替换为 50(未使用)5
n => n + 155 + 1 = 6

React 将 6 存储为最终结果,并从 useState 返回它。

🌐 React stores 6 as the final result and returns it from useState.

注意

你可能已经注意到 setState(5) 实际上像 setState(n => 5) 一样工作,但 n 未被使用!

🌐 You may have noticed that setState(5) actually works like setState(n => 5), but n is unused!

如果在更新状态后替换它会发生什么

🌐 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:

  1. setNumber(number + 5)number0,所以 setNumber(0 + 5)。React 将 “替换为 5 添加到它的队列中。
  2. setNumber(n => n + 1)n => n + 1 是一个更新函数。React 会将该函数加入它的队列。
  3. setNumber(42):React 将 “replace with 42 添加到它的队列中。

在下一次渲染期间,React 遍历状态队列:

🌐 During the next render, React goes through the state queue:

排队更新n返回值
“替换为 50(未使用)5
n => n + 155 + 1 = 6
“替换为 426(未使用)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)被添加到队列中。
  • 任何其他值(例如数字 5)会将“替换为 5”添加到队列中,而忽略已排队的内容。

在事件处理程序完成后,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).

回顾

  • 设置状态不会更改现有渲染中的变量,但它会请求一个新的渲染。
  • React 在事件处理程序运行完毕后处理状态更新。这称为批处理。
  • 要在一个事件中多次更新某个状态,你可以使用 setNumber(n => n + 1) 更新函数。

挑战 1 of 2:
修复请求计数器

🌐 Fix a request counter

你正在开发一个艺术品市场应用,该应用允许用户同时提交某个艺术品的多个订单。每次用户点击“购买”按钮,“待处理”计数器应增加一。三秒钟后,“待处理”计数器应减少,而“已完成”计数器应增加。

🌐 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);
  });
}