React是否保持状态更新的顺序?

190

我知道React可能会异步且批量执行状态更新以进行性能优化。因此,您永远不能相信调用setState后状态是否已更新。但是,您可以相信React会按照与setState调用相同的顺序更新state吗?

  1. 在同一个组件中?
  2. 在不同的组件中?

考虑单击以下示例中的按钮:

1. 对于以下情况,a为false且b为true的可能性是否存在

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: false, b: false };
  }

  render() {
    return <Button onClick={this.handleClick}/>
  }

  handleClick = () => {
    this.setState({ a: true });
    this.setState({ b: true });
  }
}

2. 以下情况中是否有可能是 a 为假且 b 为真 :

class SuperContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: false };
  }

  render() {
    return <Container setParentState={this.setState.bind(this)}/>
  }
}

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = { b: false };
  }

  render() {
    return <Button onClick={this.handleClick}/>
  }

  handleClick = () => {
    this.props.setParentState({ a: true });
    this.setState({ b: true });
  }
}

请记住,这些是我使用情况的极端简化。我认识到我可以以不同的方式完成这个任务,例如在示例1中同时更新两个状态参数,以及在示例2中将第二个状态更新作为回调执行第一个状态更新。然而,这不是我的问题,我只对React如何执行这些状态更新有兴趣,其他内容不感兴趣。

任何有文档支持的答案都将受到赞赏。


1
请查看此链接:https://dev59.com/m1oV5IYBdhLWcg3wkfe9#36087156 - helb
4
这似乎不是一个无意义的问题,你也可以在React页面的Github问题中提出这个问题,Dan Abramov通常会很乐意帮助解答。当我遇到棘手的问题时,我会向他提问,他会及时回应。不好的一点是,这种问题并没有像官方文档那样公开共享(以便其他人也能轻松获取)。我也觉得React官方文档缺乏对某些主题的广泛涵盖,比如你所问的主题等等。 - Giorgi Moniava
1
例如,参考这个链接:https://github.com/facebook/react/issues/11793,我相信该问题中讨论的内容对许多开发人员都有帮助,但是这些内容并未包含在官方文档中,因为Facebook的开发人员认为这些内容比较高级。其他可能也是如此。我认为,一个官方文章标题可以是“深入探讨React中的状态管理”或“状态管理的陷阱”,探讨像你提出的所有角落案例,这样的文章不错。也许我们可以推动Facebook的开发人员用这样的内容扩展文档 :) - Giorgi Moniava
这里有一篇关于我的问题的优秀文章链接,它应该涵盖95%的状态使用情况。 :) - Michal
2
@Michal,但是在我看来,那篇文章仍然没有回答这个问题。 - Giorgi Moniava
@Giorgi Moniava 为什么这样说呢?你的 handleClick 方法将被批处理,所以你不必担心会出现 a=trueb=false 的情况。 - Michal
5个回答

449

我在使用React进行开发。

简而言之:

但是你可以相信React会按照调用setState的顺序更新状态吗?

  • 在同一个组件中?

可以的。

  • 在不同的组件中?

也可以。

更新的顺序总是被尊重的。你是否看到中间状态"之间"取决于你是否在批处理内部。

在React 17以及之前版本中,默认情况下仅会批量处理React事件处理程序内的更新。对于需要的罕见情况,有一种不稳定的API可以在事件处理程序之外强制执行批处理。

从React 18开始,默认情况下,React 会批量处理所有更新请注意,React永远不会批处理两个不同的意图事件(例如点击或输入),因此,例如两个不同的按钮单击永远不会被批处理。在非常罕见的情况下,如果不希望批处理,则可以使用flushSync


理解这一点的关键在于无论你在多少个组件中做了多少次setState()调用,它们都将在事件结束时产生单个重新渲染。这对于大型应用程序的良好性能至关重要,因为如果在处理单击事件时ChildParent各自调用setState(),则不希望两次重新渲染Child

在您的两个示例中,setState()调用发生在React事件处理程序内部。因此,在事件结束时,它们总是一起刷新(且您不会看到中间状态)。

更新总是按照它们发生的顺序进行浅层合并。因此,如果第一个更新是{a: 10},第二个更新是{b: 20},第三个更新是{a: 30},则呈现的状态将是{a: 30, b: 20}。同一状态键(例如我的示例中的a)的最近更新始终“获胜”。

this.state对象在批处理结束时重新渲染UI时更新。因此,如果您需要基于先前的状态更新状态(例如递增计数器),则应使用提供先前状态的函数式setState(fn)版本,而不是从this.state中读取。如果您对此有疑问,请参阅我在此评论中详细解释的原因
在您的示例中,我们将无法看到“中间状态”,因为我们处于启用批处理的React事件处理程序中(因为React“知道”我们何时退出该事件)。
但是,在React 17和早期版本中,默认情况下在React事件处理程序之外没有批处理。因此,如果在您的示例中,我们有一个AJAX响应处理程序而不是handleClick,则每个setState()将立即处理。在这种情况下,是的,在React 17和早期版本中,您看到中间状态。
promise.then(() => {
  // We're not in an event handler, so these are flushed separately.
  this.setState({a: true}); // Re-renders with {a: true, b: false }
  this.setState({b: true}); // Re-renders with {a: true, b: true }
  this.props.setParentState(); // Re-renders the parent
});

我们意识到在事件处理程序中和其他情况下行为不同是很不方便的。在React 18中,这将不再是必需的,但在此之前,有一个API可以用于强制进行批处理:

promise.then(() => {
  // Forces batching
  ReactDOM.unstable_batchedUpdates(() => {
    this.setState({a: true}); // Doesn't re-render yet
    this.setState({b: true}); // Doesn't re-render yet
    this.props.setParentState(); // Doesn't re-render yet
  });
  // When we exit unstable_batchedUpdates, re-renders once
});

在React内部,所有事件处理程序都被包装在unstable_batchedUpdates中,这就是它们默认批处理的原因。请注意,将更新两次包装在unstable_batchedUpdates中没有任何效果。当我们退出最外层的unstable_batchedUpdates调用时,更新将被刷新。
该API在某个重大版本(18之后的19或更高版本)中最终会被删除,因此在React 18之前,如果您需要在React事件处理程序之外的某些情况下强制进行批处理,可以安全地依赖它。使用React 18,您可以将其删除,因为它不再起作用。
总之,这是一个令人困惑的话题,因为React以前只在事件处理程序内默认进行批处理。但解决方案不是“减少批处理”,而是默认情况下“增加批处理”。这就是React 18所做的。

3
保证“始终正确获取顺序”的一种方法是创建一个临时对象,分配不同的值(例如 obj.a = true; obj.b = true),然后最后只需执行 this.setState(obj)。无论您是否在事件处理程序中,这都是安全的。如果您经常犯在事件处理程序外多次设置状态的错误,那么这可能是一个不错的技巧。 - Chris
1
顺便提一下,这也意味着我们根本不应该从this.state中读取任何内容;读取某个state.X的唯一合理方式是在更新函数中从其参数中读取。而且,写入this.state也是不安全的。那么为什么还允许访问this.state呢?这些问题可能应该单独提出来,但我主要是想弄清楚我是否正确理解了解释。 - Maksim Gumerov
24
这个答案应该被添加到reactjs.org文档中。 - Deen John
3
请问您能否在这篇文章中澄清一下,作为React 16的事件处理程序是否包括componentDidUpdate和其他生命周期回调函数?提前致谢! - Ivan
1
已更新 React 18 的答案:http://localhost:8000/blog/2022/03/29/react-v18.html#new-feature-automatic-batching - Dan Abramov
显示剩余13条评论

9
这实际上是一个非常有趣的问题,但答案不应该太复杂。这篇medium上的文章提供了一个很好的答案。
1)如果您这样做
this.setState({ a: true });
this.setState({ b: true });

我认为由于批处理,不会出现atruebfalse的情况。
然而,如果b依赖于a,那么可能会出现意料之外的状态。
// assuming this.state = { value: 0 };
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});

在处理完上述所有调用之后,this.state.value 的值将为1,而不是您期望的3。
文章中提到了这一点:setState接受一个函数作为参数
// assuming this.state = { value: 0 };
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));

这将给我们 this.state.value === 3


如果this.state.value在事件处理程序(其中setState被批处理)和AJAX回调(其中setState被批处理)中都被更新,会发生什么情况?在事件处理程序中,我将使用updater function来确保始终使用函数提供的当前更新状态来更新状态。即使我知道它没有被批处理,我是否应该在AJAX回调的代码中使用带有更新程序函数的setState?您能否澄清在AJAX回调中使用或不使用更新程序函数的setState的用法?谢谢! - tonix
@Michal,嗨Michal,我只是想问一个问题,如果我们有这个setState({ value: 0}); setState({ value: this.state.value + 1}); 第一个setState会被忽略,只有第二个setState会被执行,这是真的吗? - Dickens
@Dickens 我认为两个 setState 都会被执行,但最后一个会胜出。 - Michal

5

同一周期内可以将多个调用批处理在一起。例如,如果您尝试在同一周期内多次增加项目数量,则将导致等效于:

Object.assign(
  previousState,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)

https://reactjs.org/docs/react-component.html


5
文档中提到,setState()方法会将组件状态的更改加入队列,并告诉React需要使用更新后的状态重新呈现此组件及其子组件。这是您在响应事件处理程序和服务器响应时更新用户界面的主要方法。
它会按照队列(先进先出:FIFO)中的方式执行更改,第一个调用将首先执行。

嗨,阿里,我只是想问一个问题,如果我们有这样的代码:this.setState({ value: 0}); this.setState({ value: this.state.value + 1}); 那么第一个setState会被忽略,只有第二个setState会被执行,这是真的吗? - Dickens

0
在这种情况下,它并没有。我有三个框和那些状态以及handleClick。
const [openedIndex, setOpenedIndex] = useState(-1);
const handleClic = (index) => {
console.log("opened index to test delayed state update", openedIndex);
if (index === openedIndex) {
  setOpenedIndex(-1);
} else {
  setOpenedIndex(index);
}
};

当我点击打开和关闭时,它可以正常工作:

enter image description here

如果我获取了一个元素的引用并在它打开时运行$0.click(),它就会关闭。

enter image description here

现在的情况是,如果连续运行$0.click()两次,它应该打开然后关闭,但事实并非如此

enter image description here

类似的文章在这里:Reactjs中更改下拉列表数据更新有延迟 React组件上onClick可以正常运行,但无法识别onDoubleClick


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接