【问题标题】:Do I need to use setState(function) overload in this case?在这种情况下我需要使用 setState(function) 重载吗?
【发布时间】:2017-09-11 17:05:33
【问题描述】:

想象一下这种情况:

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 调用987654324@ 正在等待中,这是有效的假设吗?

现在,我的问题是,如果我按照以下方式重写上面的代码(基本上使用带有回调参数的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。更一般地说,在我的第一个案例或第二个版本中,状态可能会出现什么问题(从同步的角度来看),我们将不胜感激,以便更好地了解如何在反应中管理状态。

【问题讨论】:

    标签: javascript reactjs


    【解决方案1】:

    这是一个很长的答案。如果您不想全部阅读,请向下滚动到底部的 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);
        }
    }
    

    然后,在渲染方法中,您可以显示单元格是否位于openedCellsfirstSelectedCellsecondSelectedCell

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

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

    【讨论】:

    • setState 刷新时我们还有什么信息吗? (即对状态应用更改;例如,可能在处理函数的末尾?)还是 setState(fn) 立即应用更改?添加此信息将进一步使您的答案有用。
    • setState() 不保证同步“刷新”。相反,状态更改是排队的,并且可以批量处理,这就是setState() 的对象形式可能导致依赖于当前状态的更改的错误值的原因。使用 updater 函数形式,它的行为更像 reduce,这意味着每次都会将 state 的真实当前值传递给 updater,最终的 state 值将代表对 updater 的所有调用的累积——改变取决于当前状态将起作用。
    • @EricElliott 我问丹 什么时候(如果有的话)我们能知道 setState(以对象或回调形式)刷新吗?即应用更改。例如,可能在事件处理函数的末尾,等等。
    • 当状态改变被应用时,有两种方式可以得到通知。推荐的方法是使用 Dan 在他的示例中演示的 .componentDidUpdate() 方法。第二个是 setState() 函数的 callback 参数。 .componentDidUpdate() 被认为是更清洁的解决方案。
    • @DanAbramov Dan 你怎么解释这里:codepad.org/a6hfXlfn,如果你点击 + 按钮,值只会增加一次,即使处理程序被调用了两次。既然您在回答中说从单击处理程序中读取状态是安全的? (代码取自这里:stackoverflow.com/a/42994068/3963067 - 单击底部的显示代码 sn-p)。如果您说的是真的,那么在单击 + 之后,值必须增加两次,不是吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-12
    • 2012-07-01
    • 1970-01-01
    相关资源
    最近更新 更多