小编典典

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

all

我知道 React 可能会异步和批量执行状态更新以优化性能。因此,您永远不能相信在调用setState. 但是你能相信 React 会按照 要求
顺序更新状态吗?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 是否有明确定义的方式来执行这些状态更新感兴趣,仅此而已。

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


阅读 56

收藏
2022-08-07

共1个答案

小编典典

我在 React 上工作。

TLDR:

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

  • 相同的组件?

是的。

  • 不同的组件?

是的。

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

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

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


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

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

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

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


在您的示例中,我们不会看到“中间状态”,因为我们 在启用批处理的 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 过去只批处理内部事件处理程序。但解决方案不是 批量少 ,而是默认 批量多
。这就是我们在 React 18 中所做的。

2022-08-07