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

IT技术 javascript reactjs
2021-04-16 17:28:11

想象一下这样的情况:

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(第二次重写版本)?

附:clickHandler只是改变状态的函数 - 假设当用户单击某些按钮时被调用。

pps。更一般地说,在我的第一个案例或第二个版本中,状态可能会出现什么问题(从同步的角度来看),详细的答案将不胜感激,以便更好地理解如何在react中管理状态。

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

注意:评论中指出了另一个边缘情况:如果你有多个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);

现在,有些情况下您想根据之前的 state更新state虽然您可以刚刚读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()火灾,而且它可以根据运行超时逻辑是如何的状态改变。

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

我认为如果将这个逻辑中与超时相关的部分拆分到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);
    }
}

然后,在 render 方法中,您可以显示单元格是否在openedCells或它们是firstSelectedCellsecondSelectedCell

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

  • 至少在 React 16(或更早版本)中,在事件处理程序中this.state的第一次setState()调用之前读取将为您提供当前状态。但是,不要指望它来更新setState()
  • 的函数重载setState()防止了这个陷阱,但它要求传递的函数是纯函数。设置超时并不纯粹。
  • componentDidUpdate()生命周期挂钩可能是用于设置取决于状态超时一个更好的地方。
有两种方法可以在应用状态更改时收到通知。推荐的方法是使用.componentDidUpdate()Dan 在他的例子中演示方法。第二个是函数callback参数setState().componentDidUpdate()被认为是更清洁的解决方案。
2021-05-22 17:28:11
当 setState 刷新时,我们还有任何信息吗?(即对状态应用更改;例如可能在处理程序函数的末尾?)还是 setState(fn) 立即应用更改?添加此信息将进一步使您的答案有用。
2021-06-04 17:28:11
setState()不能保证同步“刷新”。相反,状态更改是排队的,并且可以批量处理,这就是为什么 的对象形式setState()可能导致依赖于当前状态的更改值错误的原因。使用updater函数形式,它的行为更像reduce,这意味着状态的真实当前值每次都会传入updater,最终状态值将代表对updater的所有调用的累积——改变取决于当前状态将起作用。
2021-06-10 17:28:11
@EricElliott 我问丹什么时候(如果有的话)我们可以知道 setState(以对象或回调形式)刷新吗?即应用更改。例如,可能在事件处理函数的末尾等。
2021-06-13 17:28:11
@DanAbramov Dan 你如何在这里解释:codepad.org/a6hfXlfn,如果你点击 + 按钮,值只会增加一次,即使处理程序被调用两次。既然您在回答中说从 click Handler 读取状态是安全的?(代码取自此处:stackoverflow.com/a/42994068/3963067 - 单击底部的显示代码片段)。如果你说的是真的那么值必须增加两次,点击+后,不是吗?
2021-06-13 17:28:11