【问题标题】:React: potential race condition for Controlled ComponentsReact:受控组件的潜在竞争条件
【发布时间】:2018-11-15 09:43:31
【问题描述】:

the React tutorial中有如下代码:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

还有a warning关于setState方法:

setState() 并不总是立即更新组件。它可能 批处理或推迟更新。这使得阅读 this.state 在调用 setState() 之后,这是一个潜在的陷阱。

问:是否可能出现以下情况:

  1. handleChange 被触发;
  2. setState 在 React 中排队;
  3. handleSubmit 被触发,它读取 this.state.value 的过时值;
  4. setState 实际处理完毕。

或者有某种保护措施防止这种情况发生?

【问题讨论】:

    标签: javascript reactjs


    【解决方案1】:

    希望this 回答你的问题:

    在 React 16 中,如果你在 React 事件处理程序中调用 setState,它会在 React 退出浏览器事件处理程序时被刷新。所以它不是同步的,而是发生在同一个顶层堆栈中。

    在 React 16 中,如果你在 React 事件处理程序之外调用 setState,它会立即被刷新。

    让我们看看会发生什么(要点):

    1. 进入handleChange反应事件处理程序;
    2. 所有setState 调用都在内部进行批处理;
    3. 退出handleChange
    4. 刷新setState 更改
    5. render 被调用
    6. 进入handleSubmit
    7. this.state访问正确提交的值
    8. 退出handleSubmit

    如您所见,只要在React 事件处理程序中安排更新,就不会发生竞争条件,因为React 在每个事件处理程序调用结束时提交所有批处理的state 更新。

    【讨论】:

      【解决方案2】:

      在您的情况下,读取旧值是不可能的。在“它可能会批量更新或将更新推迟到以后”下,这只是一个案例

      this.setState({a: 11});
      console.log(this.state.a); 
      

      所以setState 可能只是将更改添加到队列中,而不是直接更新this.state。但这并不意味着您可以通过触发handleChange 更改输入,然后单击按钮触发handleSubmit.state 仍然没有更新。这是因为event loop 的工作原理 - 如果某些代码正在执行,浏览器将不会处理任何事件(您应该会遇到 UI 冻结一段时间的情况)。

      因此,重现“竞争条件”的唯一方法是从另一个处理程序运行一个处理程序:

      handleChange(event) {
        this.setState({value: event.target.value});
        this.handleSubmit();
      }
      

      这样,是的,您将在警报中显示以前的value

      对于这种情况.setState 采用可选的回调参数

      setState() 的第二个参数是一个可选的回调函数,一旦 setState 完成并重新渲染组件,就会执行该回调函数。通常我们建议使用 componentDidUpdate() 来代替此类逻辑。

      应用到你的代码看起来像

      handleChange(event) {
        this.setState({value: event.target.value}, this.handleSubmit);
      }
      

      PS 并且确保您的代码可能会延迟 setStatesetTimeout 您自己喜欢

      handleChange({target: {value}}) {
          setTimeout(() => {
              this.setState({value});
          }, 5000);
      }
      

      并且无法确保handleSubmit 以最新值运行。但在这种情况下,如何处理就完全取决于您了。

      [UPD] 一些关于异步如何在 JS 中工作的细节。我听说过不同的术语:“事件循环”、“消息队列”、“微任务/任务队列”,有时它意味着不同的东西(实际上是 is a difference)。但是为了让事情变得更容易,让我们假设只有一个队列。并且所有异步事件处理程序,Promise.then()setImmediate() 都进入这个队列的末尾。

      这样每个setState(如果它处于批处理模式)做两件事:将变更集添加到堆栈(它可以是数组变量)并将附加任务设置到队列中(比如setImmediate)。此附加任务将处理所有堆叠的更改并只运行一次重新渲染。

      即使您在那些延迟更新程序执行之前如此快地单击提交按钮,事件处理程序也会进入队列末尾。因此,事件处理程序肯定会在应用所有批处理状态的更改后运行。

      抱歉,我不能只参考 React 代码来证明,因为更新程序代码对我来说看起来真的很复杂。但我发现 article that has many details 它是如何在幕后工作的。也许它会给你一些额外的信息。

      [UPD] 遇到了关于微任务、宏任务和事件循环的好文章:https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f?gi=599c66cc504c 它不会改变结果,但让我更好地理解所有这些

      【讨论】:

      • 对我来说变得更清楚了,但还不是水晶。你是说 React 将 setState 调用排队到一些 保证 在处理事件之间处理的内部 JS 查询?
      • 在查看了我自己的答案后,我发现它看起来过于复杂。所以我更新了更多细节的答案。
      • 你和凯伦的回答都很棒,我很难决定奖励谁。我已经接受了凯伦的回答,因为它更简洁。但是赏金会更详细地提供您的答案,并包含有用的参考资料以深入调查此问题。
      • 找到了关于微任务/宏任务/事件循环的好文章:abc.danch.me/…
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-30
      • 2021-05-31
      • 1970-01-01
      相关资源
      最近更新 更多