反应:受控组件的潜在竞争条件

时间:2018-11-15 09:43:31

标签: javascript reactjs

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>
    );
  }
}

关于 setState 方法的还有a warning

  

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

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

  1. handleChange 被触发;
  2. setState 在React中排队;
  3. 触发
  4. handleSubmit ,并读取 this.state.value ;
  5. 的过时值。 实际上已经处理了
  6. setState

还是有某种保护措施可以防止这种情况的发生?

2 个答案:

答案 0 :(得分:4)

我希望this 回答您的问题:

  

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

     

在React 16中,如果您在React事件处理程序外部调用setState,则会立即将其刷新。

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

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

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

答案 1 :(得分:3)

在您的情况下,读取旧值是不可能的。在“它可能会批量更新或将更新推迟到以后”的含义是

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 它不会改变结果,但会让我更好地理解一切