我知道 React 可能会异步和批量执行状态更新以优化性能。因此,您永远不能相信在调用setState. 但是你能相信 React 会按照 要求 的 顺序更新状态吗?setState
setState
考虑单击以下示例中的按钮:
1. 是否存在 a 为假而 b 为真的 可能性:
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 是否有明确定义的方式来执行这些状态更新感兴趣,仅此而已。
非常感谢任何由文档支持的答案。
我在 React 上工作。
TLDR:
但是你能相信 React 会按照调用 setState 的顺序更新状态吗? 相同的组件?
但是你能相信 React 会按照调用 setState 的顺序更新状态吗?
是的。
不同的组件?
始终遵守更新 顺序 。您是否看到它们“之间”的中间状态取决于您是否在批次中。
在 React 17 及更早版本中, 默认情况下,只有 React 事件处理程序内的更新是批处理的 。有一个不稳定的 API 可以在您需要的极少数情况下强制在事件处理程序之外进行批处理。
从 React 18 开始,React 默认批处理所有更新。请注意,React 永远不会批量更新来自两个不同的有意事件(如点击或打字),因此,例如,两个不同的按钮点击永远不会被批量更新。在极少数不需要批处理的情况下,您可以使用flushSync.
flushSync
理解这一点的关键是, 无论您 在 React 事件处理程序setState()中调用多少组件,它们都只会在事件结束时产生一次重新渲染 __。 这对于大型应用程序中的良好性能至关重要,因为如果在处理单击事件Child时Parent每次调用setState(),您都不想重新渲染Child两次。
setState()
Child
Parent
在您的两个示例中,setState()调用都发生在 React 事件处理程序中。因此,它们总是在事件结束时一起刷新(并且您看不到中间状态)。
更新总是 按照它们发生的顺序进行浅层合并 。所以如果第一次更新是{a: 10},第二次是{b: 20},第三次是{a: 30},渲染状态将是{a: 30, b: 20}。对同一状态键的最新更新(例如a在我的示例中)总是“获胜”。
{a: 10}
{b: 20}
{a: 30}
{a: 30, b: 20}
a
当this.state我们在批处理结束时重新渲染 UI 时,对象会更新。因此,如果您需要根据先前的状态更新状态(例如递增计数器),您应该使用setState(fn)为您提供先前状态的功能版本,而不是从this.state. 如果您对此原因感到好奇,我在此评论中对其进行了深入解释。
this.state
setState(fn)
在您的示例中,我们不会看到“中间状态”,因为我们 在启用批处理的 React 事件处理程序 中(因为 React “知道”我们何时退出该事件)。
但是,在 React 17 和更早的版本中, 默认情况下在 React 事件处理程序之外没有批处理 。因此,如果在您的示例中我们有一个 AJAX 响应处理程序而不是handleClick,则每个都setState()将在发生时立即处理。在这种情况下,是的,你 会 在 React 17 及更早版本中看到一个中间状态:
handleClick
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调用时,更新被刷新。
unstable_batchedUpdates
该 API 是“不稳定的”,因为我们最终将在 18 之后的某个主要版本(19 或更高版本)中将其删除。如果您需要在 React 事件处理程序之外的某些情况下强制批处理,您可以安全地依赖它直到 React 18。使用 React 18,您可以删除它,因为它不再有任何效果。
总而言之,这是一个令人困惑的话题,因为默认情况下 React 过去只批处理内部事件处理程序。但解决方案不是 批量少 ,而是默认 批量多 。这就是我们在 React 18 中所做的。