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

2022-08-30 01:47:19

我知道 React 可以异步和批量执行状态更新以进行性能优化。因此,您永远不能相信在调用 后状态会更新。但是你能相信 React 会按照与 setState 调用相同的顺序更新状态吗?setState

  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 执行这些状态更新的明确定义的方式感兴趣,仅此而已。

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


答案 1

我在 React 上工作。

TLDR:

但是你能相信 React 会按照与 setState 调用相同的顺序更新状态吗?

  • 相同的组件?

是的。

  • 不同的组件?

是的。

始终遵守更新的顺序。你是否看到它们之间的中间状态取决于你是否在一个批次中。

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

从 React 18 开始,React 默认对所有更新进行批处理请注意,React 永远不会从两个不同的有意事件(如点击或键入)中批量更新,因此,例如,两个不同的按钮点击永远不会被批处理。在极少数情况下,不需要批处理,则可以使用 flushSync


理解这一点的关键是,无论你在 React 事件处理程序中执行多少个组件中有多少 setState() 调用,它们只会在事件结束时生成一个重新呈现。这对于在大型应用程序中获得良好的性能至关重要,因为在处理单击事件时,如果和每个调用,则不希望重新呈现两次。ChildParentsetState()Child

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

更新始终按其发生的顺序浅层合并。因此,如果第一次更新是 ,第二次是 ,第三次更新是 ,则呈现状态将为 。最近对相同状态键的更新(例如,如我的例子中)总是“获胜”。{a: 10}{b: 20}{a: 30}{a: 30, b: 20}a

当我们在批处理结束时重新呈现 UI 时,对象将更新。因此,如果需要基于以前的状态更新状态(例如递增计数器),则应使用提供以前状态的功能版本,而不是从 中读取 。如果你对其中的原因感到好奇,我在这个评论中对此进行了深入的解释。this.statesetState(fn)this.state


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

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

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_batchedUpdatesunstable_batchedUpdatesunstable_batchedUpdates

该API是“不稳定的”,因为我们最终将在18(19或更远)之后的某个主要版本中将其删除。在 React 18 之前,如果你需要在 React 事件处理程序之外的某些情况下强制批处理,则可以安全地依赖它。使用 React 18,你可以删除它,因为它不再有任何影响。


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


答案 2

这实际上是一个非常有趣的问题,但答案不应该太复杂。有一篇关于媒介的好文章有一个答案。

1) 如果您这样做

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

我不认为会因为批处理而出现这种情况。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});

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

这在文章中提到: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