聚合国内IT技术精华文章,分享IT技术精华,帮助IT从业人士成长

React 是否保持 state 更新的顺序?

2018-04-25 13:55 浏览: 1402923 次 我要评论(0 条) 字号:

stackoverflow 有人提问:Does React keep the order for state updates?

我知道 React 的状态更新是异步执行的,为了性能优化,状态是批量更新的。所以你永远不能确信在调用 setState 后状态是否更新了。但是你是否可以确认 setState 调用后状态的更新顺序呢?

比如以下情况:

  • 相同的组件?
  • 不同的组件?

考虑以下按钮点击的例子:

  1. 是否有可能 afalsebtrue?
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 });
  }
}
  1. 是否有可能 afalsebtrue?
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 中同时更新两个 state 值,以及在例 2 中的第一个 setState 的回调中执行第二个 setState。但是,这不是我的问题,而且我只想知道 React 执行这些状态更新时有没有确切的方式和顺序,仅此而已。


不久后,React 的作者 Dan Abramov 亲自来回答了。

I work on React.

开场定乾坤。很有力度的一句话。

TLDR:

很多英文文章中都会出现 TLDR,有时候会写成 tl_dr、tl;dr,是 Too Long; Didn’t Read 的缩写,作用就是告诉读者,这篇内容篇幅比较长,如果不想深入探讨或时间有限,可以看总结。

Dan Abramov 先给出了结论:

  • 同一个组件中,setState 是否有确定的顺序?是的
  • 不同组件中,setState 是否有确定的顺序?是的

状态始终是按照特定的顺序更新的。无论你是否看到介于两个状态之间的一个中间状态,无论你是否在批处理内。

目前(React 16 及更早版本),默认情况下,只有 React 事件处理程序中的更新才会被批处理。有一个不稳定(unstable)的 API 来强制在事件处理程序之外进行批处理,以便在需要时处理罕见的情况。

在未来的版本(可能是 React 17 或更高版本)中,React 将默认批量更新所有更新,因此你不必考虑这一点。与往常一样,我们将在 React 博客和 release note 中宣布对此的任何更改。


理解这一点的关键在于,在 React 事件处理程序中,不论 setState() 调用了多少次,也不论 setState() 被多少个组件调用,它们在事件结束时只会生成一次重新渲染。这对于大型应用程序性能至关重要,因为如果 ChildParent 处理 click 事件的时候都调用 setState(),你不希望重新渲染 Child 两次。

在这两个例子中,setState() 调用都发生在 React 事件处理程序中。因此,在事件结束时,他们总是被合并到一起(而且你看不到中间状态)。

更新总是按照它们发生的顺序进行浅合并(shallowly merge)。所以如果第一次更新是 {a: 10},第二次是 {b: 20},第三次 {a: 30},呈现的状态将是 {a: 30, b: **20}。对 state 中同一个 key 的更新(例如我的例子中 a 的更新),最新(近)的更新总是“胜出”。**

当我们在批处理结束时重新呈现 UI 时,this.state 对象已经被更新了。所以如果你需要根据之前的状态更新状态(比如增加一个计数器),你应该使用 setState(fn) 的回掉函数版本来提供之前的状态,而不是读取状态 this.state。如果你对这个理由感到好奇,我在这个评论中深入地解释了它。


在你的例子中,我们不会看到“中间状态”,因为我们在具有批处理功能的 React 事件处理程序中(因为 React “知道”什么时候退出该事件)。

但是,在 React 16 和更早版本中,React 事件处理程序之外还没有默认的批处理。因此,如果在你的例子中,我们吧 handleClick 替换为 AJAX 处理程序,那么每个 setState() 都会立即处理。在这种情况下,是的,你看到一个中间状态:

promise.then(() => {
  // 我们不在事件处理程序中,因此它们都会被刷新。
  this.setState({a: true}); // 使用 {a: true, b: false } 重新渲染
  this.setState({b: true}); // 使用 {a: true, b: true } 重新渲染
  this.props.setParentState(); // 重新渲染父组件
});

我们认识到,根据是否处于事件处理程序中,行为是不同的,这是不方便的。这将在未来的 React 版本中进行更改,默认情况下将批量更新所有更新(并提供选择性 API 以同步刷新更改)。直到我们切换默认行为(可能在 React 17 中),有一个 API 可以用来强制批量处理:

promise.then(() => {
  // 强制批量处理
  ReactDOM.unstable_batchedUpdates(() => {
    this.setState({a: true}); // 不重新渲染
    this.setState({b: true}); // 不重新渲染
    this.props.setParentState(); // 不重新渲染
  });
  // 当我们退出 unstable_batchedUpdates函数后,重新渲染一次
});

在 React 内部,事件处理程序都被包装在 unstable_batchedUpdates 内,这就是默认情况下批处理的原因。请注意,将 setState 封装 unstable_batchedUpdates 两次是不起作用的。当我们退出最外层的unstable_batchedUpdates 调用时,更新被刷新。

该 API 是“不稳定的”,因为如果默认情况下已经启用批处理,我们将删除它。但是,在 React 小版本中我们不会删除它,所以在 React 17 之前,如果你需要在 React 事件处理程序之外的某些情况下强制批处理,你可以安全地依赖它。


总之,这是一个令人困惑的话题,因为 React 默认只在事件处理程序中进行批处理。这将在未来的版本中发生变化,那么行为将更直接。但解决方案不是减少批处理,而是默认开启更多的批处理。这就是我们要做的。



网友评论已有0条评论, 我也要评论

发表评论

*

* (保密)

Ctrl+Enter 快捷回复