第二四章 添加交互-对一系列状态更新进行排队

2023-02-13  本文已影响0人  深圳都这么冷

对一系列状态更新进行排队

设置状态变量将使另一个渲染排队。 但有时您可能希望在排队下一次渲染之前对值执行多个操作。 为此,有助于了解 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 被称为更新函数。 当您将它传递给状态设置器时:

  1. 在事件处理程序中的所有其他代码运行之后,React 将此函数排队等待处理。
  2. 在下一次渲染期间,React 遍历队列并为您提供最终的更新状态。
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

以下是 React 在执行事件处理程序时如何处理这些代码行:

  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 会遍历队列。 之前的数字状态是 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 要做的事情:

  1. setNumber(number + 5): number为0,所以setNumber(0 + 5)。 React 将“替换为 5”添加到其队列中。
  2. 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 在执行此事件处理程序时如何处理这些代码行:

  1. setNumber(number + 5): number为0,所以setNumber(0 + 5)。 React 将“替换为 5”添加到其队列中。
  2. setNumber(n => n + 1): n => n + 1 是一个更新函数。 React 将该函数添加到它的队列中。
  3. 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 状态设置器的内容:

事件处理程序完成后,React 将触发重新渲染。 在重新渲染期间,React 将处理队列。 更新函数在渲染期间运行,因此更新函数必须是纯函数并且只返回结果。 不要尝试从它们内部设置状态或运行其他副作用。 在严格模式下,React 将运行每个更新程序函数两次(但丢弃第二次结果)以帮助您发现错误。

命名约定

通常用相应状态变量的首字母命名更新函数参数:

setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

如果您喜欢更冗长的代码,另一个常见的约定是重复完整的状态变量名称,例如 setEnabled(enabled => !enabled),或使用前缀,例如 setEnabled(prevEnabled => !prevEnabled)。

回顾

上一篇 下一篇

猜你喜欢

热点阅读