这是一个很长的答案。如果您不想全部阅读,请向下滚动到底部的 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 或firstSelectedCell 或secondSelectedCell。
我希望这会有所帮助!
总而言之,这是 TLDR:
- 至少在 React 16(或更早版本)中,在事件处理程序中的第一个
setState() 调用之前读取 this.state 将为您提供当前状态。但不要指望它会在setState() 之后立即更新。
-
setState() 的函数重载可以防止这个陷阱,但它要求传递的函数是纯函数。设置超时不是纯粹的。
-
componentDidUpdate() 生命周期挂钩可能是设置取决于状态的超时的更好位置。