在这种情况下,我是否需要使用setState(function)重载?

时间:2017-04-15 16:28:34

标签: javascript reactjs

想象一下这种情况:

clickHandler(event) {
  var that = this;
  var cellIndex = event.target.id;

  if(that.inProcess) return;

  /* Temporarily clone state */
  var cloneState = JSON.parse(JSON.stringify(this.state));

  var currentCell = cloneState.gameCellArray[cellIndex];
  currentCell.shown = true;

  if (that.someCondition) {

          that.inProcess = true;
          setTimeout(function () {

              currentCell.shown = false;
              that.setState(cloneState);
              that.inProcess = false;

          }, 1000)

  }
  this.setState(cloneState);

}

现在我在理论上担心我克隆状态(clickHandler中的第四个陈述)可能会发生我无法获得最新版本的状态 - 因为有些setState来自先前调用clickHandler的调用正在等待,这是否是有效的假设?

现在,我的问题是,如果我以下面的方式重写上面的代码(基本上使用带有回调参数的setState),它会更安全吗? (就没有失去同步状态而言)。

clickHandler(event) {
    var that = this;
    var cellIndex = event.target.id;
    if(that.inProcess) return;

    that.setState(function(prevState, props){

        var cloneState = JSON.parse(JSON.stringify(prevState));

        var currentCell = cloneState.gameCellArray[cellIndex];
        currentCell.shown = true;

        if (that.someCondition) {

                that.inProcess = true;
                setTimeout(function () {
                    currentCell.shown = false;
                    // Do I need to take some measures here also?
                    that.setState(cloneState);
                    that.inProcess = false;

                }, 1000)

        }
        return cloneState;

    })

}

我是否还需要在setTimeout(第二个重写版本)中采取一些额外措施?

PS。 clickHandler只是改变状态的功能 - 当用户点击某些按钮时,它们会被调用。

PPS。更一般地说,从我的第一个案例或第二个版本中可能出现的问题(从同步的角度来看)和状态,可以理解详细的答案,以便更好地理解如何在反应中管理状态。

1 个答案:

答案 0 :(得分:60)

这是一个很长的答案。如果您不想全部阅读,请向下滚动到底部的TLDR。

请注意,我描述了可能在React 17+中更改的一些实现细节。这就是为什么我们的文档更加模糊,以致大多数人不太依赖实现细节。但在这种情况下,你似乎对真正的工作方式特别感兴趣,所以我必须比我想要的更具体。

  

现在我在理论上担心我克隆状态(clickHandler中的第三个语句)可能会发生我无法获得最新版本的状态 - 因为之前调用clickHandler的一些setState调用正在等待,这是有效的假设吗?

没有。在回复时(React 16和任何早期版本),事件处理程序中的this.state在您自己更新状态之前可以安全地阅读 。所以这段代码很好:

handleClick() {
  var something = this.state.something;

它将为您提供当前状态。

唯一的缺陷是,如果您自己致电setState,则不应期望this.state立即更新。所以这段代码不起作用:

handleClick(e) {
  this.setState({ something: e.target.value });
  var something = this.state.something; // Don't expect to get newValue here
  

注意:还有另一个边缘情况pointed out in the comments:如果你有几个onClick处理程序,同样的陷阱适用:一旦你在子事件处理程序中调用setState(),你就可以'在父事件处理程序运行时,依赖于this.state更新。实际上,这就是优化非常有用的原因:来自单个浏览器事件的所有setState()调用都是批处理的,无论它们是在事件冒泡时发生在一个组件还是不同的组件中。

不过,这不是问题,因为如果您致电setState,您已经知道自己将其设置为:

handleClick(e) {
  var newValue = e.target.value;
  this.setState({ something: newValue });
  // There's no need to "read" something from state.
  // Since you just set it, you already *know*
  // what you set it to. For example:
  doSomethingWith(newValue);

现在,有些情况下您希望根据以前的状态更新状态 。虽然您可以在事件处理程序中阅读this.state,但这只能运行一次:

handleIncrement() {
  // This will increment once:
  this.setState({ counter: this.state.counter + 1 });
  // These won't work because this.state.counter isn't updated yet:
  this.setState({ counter: this.state.counter + 1 });
  this.setState({ counter: this.state.counter + 1 });

为了让您免于担心此类案例, React提供了一个接受函数的不同setState()重载。该函数将在应用更新时接收当前状态,以便您可以安全地使用它。 React将确保通过所有挂起的函数“线程化”当前状态:

function increment(prevState) {
  return { counter: prevState.counter + 1 };
}

// ...
handleIncrement() {
  // Each function in the queue will receive the right state:
  this.setState(increment);
  this.setState(increment);
  this.setState(increment);
  // So this will increment three times.

从React 16及更早版本开始,只有当您从同一个事件处理程序多次调用setState()时,此重载才有用。但是,由于它在其他情况下也适用,我们通常建议您在setState()调用取决于当前状态时使用它,这样您根本不需要考虑这一点。但是,如果您的代码在没有它的情况下工作,并且尝试重写它会使其更加混乱,请不要打扰。

将来我们也可能在更多情况下依赖它,但我们会明确地在未来版本中提出任何此类更改。我们也将为此制定一个更“自然”的API,因为我们已经注意到人们对这个矛盾感到困惑,因为setState()的明显命令性,以及我们推荐的功能性更强的方法。

在您的特定情况下,我实际上认为第一种方法更简单。您只在事件处理程序中调用setState()一次(超时发生在以后),因此多次连续调用的陷阱不适用。

使用功能setState()表单的第二种方法实际上没有正确使用它,使整个代码更加混乱。 setState()的函数形式假定您传递给它的函数是pure。例如,这是一个纯函数:

function increment(prevState) {
  return { counter: prevState.counter + 1 };
}

但是,您传递的函数不仅计算下一个状态,还会计划超时,保持状态的一部分,将其变异,并在超时内再次调用setState。这显然是纯函数的行为方式。经验法则是,如果你不在render()内做某事,你也不应该在setState()更新程序功能中执行此操作。

同样,在React 16或更低版本中,在这种特殊情况下将代码重写为函数形式并不是有益的(我解释了上述原因:你只需要调用setState()一次,而你就是不试图在它之后立即阅读国家)。但是如果你想使用功能形式,你需要确保你传递的功能是纯粹的。问题是:你在哪里放置超时逻辑呢?

我的观点是超时逻辑最好放在componentDidUpdate()生命周期钩子中。通过这种方式,只要满足必要的条件,就可以通过状态变化真正触发 - 无论发生在哪个部件中。例如,即使您有两个按钮触发相同的状态更改,它们也会导致componentDidUpdate()触发,并且它可以运行超时逻辑,具体取决于状态更改

由于你的问题是关于实现一个基于this GitHub discussion的记忆游戏,我写了一些关于如何处理这个任务的伪代码。让我在这里引用我的答案:

  

我认为如果将此逻辑的超时相关部分拆分为componentDidUpdate生命周期钩子,则代码可能更容易理解。可能还有更好的方法来模拟国家本身。匹配游戏看起来像一个“状态机”,有几个不同的有效状态(没有选中,一个项目被选中等待,两个正确的项目被选中,两个错误项目被选中)。

     

可能值得将这些可能的游戏状态更直接地编码到组件状态中,并更仔细地考虑如何用对象表示它们。例如,可能是代替单元格值数组,更容易考虑显式状态,如:

{
  openedCells: [1, 2], // array of ids
  firstSelectedCell: 5, // could be null
  secondSelectedCell: 7, // could be null
}
     

然后在componentDidUpdate中实现条件逻辑,例如

handleClick(e) {
  // Are we waiting for a timeout? Reset it.
  if (this.resetTimeout) {
    clearTimeout(this.resetTimeout);
  }

  const id = ... // get it from target node, or bind event handler to ID in render()
  this.setState(prevState => {
    if (prevState.firstSelectedCell !== null && prevState.secondSelectedCell === null) {
      // There is just one selected cell. We clicked on the second one.
      return {
        secondSelectedCell: id
      };
    }
    // We are selecting the first cell
    // (either because we clicked to reset both or because none were selected).
    return {
      firstSelectedCell: id,
      secondSelectedCell: null
    };
}

componentDidUpdate(prevState) {
  if (prevState.secondSelectedCell !== this.state.secondSelectedCell) {
    // We just picked the second cell.
    if (isSamePicture(
      this.state.secondSelectedCell,
      this.state.firstSelectedCell
    ) {
      // Same picture! Keep them open.
      this.setState(prevState => {
         // Add them both to opened cells and reset.
         return {
           firstSelectedCell: null,
           secondSelectedCell: null,
           openedCells: [
             ...prevState.openedCells,
             prevState.firstSelectedCell,
             prevState.secondSelectedCell
           ]
         };
    } else {
      // Clear both in a second.
      this.resetTimeout = setTimeout(() => {
        this.setState({
          firstSelectedCell: null,
          secondSelectedCell: null,
        });
      }, 1000);
    }
}
     

然后,在渲染方法中,您可以显示单元格,如果它们位于openedCells中,或者是firstSelectedCellsecondSelectedCell

我希望这有帮助! 总而言之,这里是TLDR

  • 至少在React 16(或更早版本)中,在事件处理程序中的第一个this.state调用之前读取setState()将为您提供当前状态。但是不要指望在setState()之后立即更新
  • setState()的功能过载可以防止这种陷阱,但它要求传递的函数是纯粹的。设置超时纯。
  • componentDidUpdate()生命周期挂钩可能是设置取决于状态的超时的更好地方。