React 是否保持状态更新的顺序?

Does React keep the order for state updates?

我知道React为了性能优化可能会异步批量更新状态。因此,您永远不能相信调用 setState 后状态会被更新。但是你能相信 React 更新状态的顺序与 setState 被调用 for

的顺序相同吗
  1. 相同的组件?
  2. 不同的成分?

考虑点击以下示例中的按钮:

1. 是否有可能 a 为假而 b 为真 对于:

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: false, b: false };
  }

  render() {
    return <Button onClick={this.handleClick}/>
  }

  handleClick = () => {
    this.setState({ a: true });
    this.setState({ b: true });
  }
}

2. 是否有可能 a 为假而 b 为真 对于:

class SuperContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: false };
  }

  render() {
    return <Container setParentState={this.setState.bind(this)}/>
  }
}

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { b: false };
  }

  render() {
    return <Button onClick={this.handleClick}/>
  }

  handleClick = () => {
    this.props.setParentState({ a: true });
    this.setState({ b: true });
  }
}

请记住,这些是我的用例的极端简化。我意识到我可以做不同的事情,例如在示例 1 中同时更新两个状态参数,以及在示例 2 中对第一次状态更新的回调中执行第二次状态更新。但是,这不是我的问题,我只对是否存在感兴趣React 执行这些状态更新的定义明确的方式,没有别的。

非常感谢任何有文档支持的答案。

同一个周期内的多个调用可以一起批处理。例如,如果您尝试在同一个周期内多次增加一个项目数量,则结果将等同于:

Object.assign(
  previousState,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)

https://reactjs.org/docs/react-component.html

这实际上是一个很有趣的问题,但答案应该不会太复杂。这位伟大的 article on medium 给出了答案。

1) 如果你这样做

this.setState({ a: true });
this.setState({ b: true });

我认为不会因为batching而出现atruebfalse的情况。

但是,如果 b 依赖于 a 那么确实可能存在无法获得预期状态的情况。

// assuming this.state = { value: 0 };
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});

处理完上述所有调用后,this.state.value 将是 1,而不是您期望的 3。

文章中提到了这个:setState accepts a function as its parameter

// assuming this.state = { value: 0 };
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));

这会给我们 this.state.value === 3

doc

setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.

它将按照队列执行更改(FIFO:先进先出)第一个调用将首先执行

我从事 React。

TLDR:

But can you trust React to update the state in the same order as setState is called for

  • the same component?

是的。

  • different components?

是的。

更新的顺序 始终受到尊重。您是否看到它们“之间”的中间状态取决于您是否在批处理中。

在 React 17 及更早版本中,默认情况下仅批处理 React 事件处理程序中的更新。有一个不稳定的 API 可以在您需要时在极少数情况下强制在事件处理程序之外进行批处理。

从 React 18 开始,React batches all updates by default. Note that React will never batch updates from two different intentional events (like clicks or typing) so, for example, two different button clicks will never get batched. In the rare cases that batching is not desirable, you can use flushSync.


理解这一点的关键是,无论您在 React 事件处理程序中对多少组件调用多少次 setState(),它们将在事件结束时仅产生一次重新渲染。这对于大型应用程序的良好性能至关重要,因为如果 ChildParent 在处理单击事件时每次调用 setState(),您不想重新呈现 Child两次。

在您的两个示例中,setState() 调用发生在 React 事件处理程序中。因此它们总是在事件结束时一起刷新(并且您看不到中间状态)。

更新总是按它们发生的顺序浅合并。所以如果第一次更新是 {a: 10},第二次是 {b: 20},第三次是 {a: 30},渲染状态将是 {a: 30, b: 20}。对同一状态键的最新更新(例如,在我的示例中为 a)总是“获胜”。

当我们在批处理结束时重新渲染 UI 时更新 this.state 对象。因此,如果您需要根据以前的状态更新状态(例如递增计数器),您应该使用为您提供以前状态的功能 setState(fn) 版本,而不是从 this.state 读取。如果你对其中的原因感到好奇,我会深入解释 in this comment


在您的示例中,我们不会看到“中间状态”,因为我们在 React 事件处理程序中,其中启用了批处理(因为 React“知道”我们何时重新退出该事件)。

但是,在 React 17 和更早的版本中,默认情况下在 React 事件处理程序之外没有批处理。因此,如果在您的示例中我们有一个 AJAX 响应处理程序而不是 handleClick,每个 setState() 都会在发生时立即处理。在这种情况下,是的,您 在 React 17 和更早版本中看到中间状态:

promise.then(() => {
  // We're not in an event handler, so these are flushed separately.
  this.setState({a: true}); // Re-renders with {a: true, b: false }
  this.setState({b: true}); // Re-renders with {a: true, b: true }
  this.props.setParentState(); // Re-renders the parent
});

我们意识到 行为会因您是否在事件处理程序中而有所不同,这很不方便。在 React 18 中,这不再是必需的,但在此之前,有一个 API 可以用来强制批处理 :

promise.then(() => {
  // Forces batching
  ReactDOM.unstable_batchedUpdates(() => {
    this.setState({a: true}); // Doesn't re-render yet
    this.setState({b: true}); // Doesn't re-render yet
    this.props.setParentState(); // Doesn't re-render yet
  });
  // When we exit unstable_batchedUpdates, re-renders once
});

内部 React 事件处理程序都被包装在 unstable_batchedUpdates 中,这就是它们默认被批处理的原因。请注意,在 unstable_batchedUpdates 中包装更新两次没有任何效果。当我们退出最外面的 unstable_batchedUpdates 调用时更新被刷新。

从某种意义上说,API 是“不稳定的”,我们最终会在 18(19 或更晚)之后的某个主要版本中将其删除。如果你需要在 React 事件处理程序之外的某些情况下强制批处理,你可以安全地依赖它直到 React 18。使用 React 18,您可以删除它,因为它不再有任何效果。


总而言之,这是一个令人困惑的话题,因为默认情况下,React 过去只对内部事件处理程序进行批处理。但解决方案不是 batch less,而是默认 batch more。这就是我们在 React 18 中所做的。