当前位置: 首页 > 面试题库 >

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

宋健柏
2023-03-14
问题内容

想象这样的情况:

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。更一般地,从我的第一种情况或第二种情况来看,状态可能出了问题(从同步的角度来看),我们将详细地回答一下,以便更好地理解如何在反应中管理状态。


问题答案:

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

现在,有些情况下您想 根据先前的状态 更新 状态 。尽管您可能刚刚读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()假定您传递给它的函数是纯函数。例如,这是一个纯函数:

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

但是,传递给您的函数不仅会计算下一个状态,而且还会安排超时,保留一部分状态,将其更改为适当的位置,然后在超时内setState再次调用。显然,这
不是 纯函数的行为。经验法则是,
如果您不打算在内部执行任何操作render(),则也不应在setState()updater函数中执行该操作

再一次,在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);
    }
}

然后,在渲染方法,可以显示电池如果他们要么是openedCells或他们firstSelectedCellsecondSelectedCell

我希望这有帮助!总结一下, 这是TLDR

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


 类似资料:
  • 问题内容: 因为调用flush()获取每个实体都从内存持久到数据库。因此,如果我使用过多的不必要的flush()调用,可能会花费很多时间,因此对于性能而言不是一个好的选择。这是一种我不知道何时调用flush()的情况? 我的问题是:是否可以避免调用No.1同花顺? 我担心的事情是:为了执行 item.setOrder(ord) ,我们需要ord的数据库ID。而且仅调用 em.persist(ord

  • 我有一个粒子模拟项目,我已经工作了很多个小时,我将发布两个类。一个是粒子类,一个是main和Canvas类。我创建一个画布,然后得到它的BufferStrategy和一个Graphics在上面绘制。我使用更新循环来更新每一帧的粒子,并使用渲染循环来渲染每一帧的粒子。更新和渲染都是通过调用粒子数组列表中每个粒子的自渲染和自更新方法来完成的。现在这是我的问题。我有一个MouseListener,它在中

  • 我用干净的架构在android项目上工作。 我有以下课程: 使用public dispose()方法从onNext实现中释放序列。 但我还是不明白使用它的好处。是否用于在销毁视图时从observable取消订阅,以便从转到并关闭发射器上的订阅?

  • 我在一个DB中有两个表(和),它们每个都有一个称为的相互列。 我当前使用以下代码仅从中导入一些数据(,): 如果我也想从导入数据(例如,名为和的列),那么我应该向该代码添加什么? 我的目标是拥有这些钥匙: 编辑: 编辑2: 仍然得到一个错误:

  • 假设我有这个语法,用Antlr4编写: 我的思维过程是应该与相同;应该是。如果 上面的树似乎是正确的,它再次计算为。 但是当不使用空格时,antlr认为有两个INT-expr。如果 不应该跳过所有空格(使用规则),这意味着两个表达式的解析方式应该相同吗?

  • 问题内容: 这可能是一个琐碎的问题,但阅读ARG和ENV的文档对我而言并不清楚。 我正在构建一个PHP-FPM容器,我想提供启用/禁用某些用户需求扩展的功能。 如果可以在Dockerfile中通过添加条件并在build命令上传递标志来做到这一点,那就太好了,但不支持AFAIK。 就我而言,我的个人方法是在容器启动时运行一个小的脚本,如下所示: 这是我的样子: 如果您需要深入了解我的工作方式,那么这