首先,我认为我们同意延迟对账以批量更新是有益的。也就是说,我们同意setState()在许多情况下同步重新渲染效率低下,如果我们知道我们可能会得到多个更新,最好批量更新。

例如,如果我们在浏览器click处理程序中,并且两者都Child调用Parent,setState我们不想重新渲染Child两次,而是更愿意将它们标记为脏,并在退出浏览器事件之前一起重新渲染它们。

你在问:为什么我们不能做同样的事情(批处理),而是setState立即写入更新this.state而不等待协调结束。我认为没有一个明显的答案(任何一种解决方案都有权衡),但这里有一些我能想到的原因。

保证内部一致性

即使state是同步更新,props也不是。props(在重新渲染父组件之前,您无法知道,如果您同步执行此操作,批处理就会消失。)

现在 React ( state, props, refs) 提供的对象在内部是一致的。这意味着如果您只使用这些对象,则可以保证它们引用完全协调的树(即使它是该树的旧版本)。为什么这很重要?

当您仅使用状态时,如果它同步刷新(如您所建议的那样),则此模式将起作用:

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

但是,假设需要提升此状态以在几个组件之间共享,因此您将其移动到父级:

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

我想强调的是,在依赖setState()于此的典型 React 应用程序中,这是您每天都会进行的最常见的一种特定于 React 的重构。

但是,这破坏了我们的代码!

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

这是因为,在您提出的模型中,this.state会立即刷新但this.props不会。而且我们不能在this.props不重新渲染父对象的情况下立即刷新,这意味着我们将不得不放弃批处理(根据情况,这会显着降低性能)。

还有一些更微妙的情况会破坏这种情况,例如,如果您正在混合来自props(尚未刷新)和state(建议立即刷新)的数据以创建新状态。Refs 提出了同样的问题。

这些例子根本不是理论上的。事实上,React Redux 绑定曾经有过这种问题,因为它们混合了 React props 和非 React 状态。

我不知道为什么 MobX 用户没有遇到这种情况,但我的直觉是他们可能会遇到这种情况,但认为他们是自己的错。或者他们可能没有从 MobX 可变对象中读取太多内容props,而是直接从 MobX 可变对象中读取。

那么今天 React 是如何解决这个问题的呢?在 React 中,只有在协调和刷新之后才更新和刷新,所以你会看到this.state在重构之前和之后都被打印出来this.props 0这使得提升状态安全。

是的,在某些情况下这可能会带来不便。特别是对于那些来自更多 OO 背景的人来说,他们只想多次改变状态,而不是考虑如何在一个地方表示完整的状态更新。我可以理解这一点,尽管我确实认为从调试的角度来看,保持状态更新的集中更加清晰。

尽管如此,您仍然可以选择将要立即读取的状态移动到某个横向可变对象中,特别是如果您不将其用作渲染的真实来源。这几乎就是 MobX 让你做的事情🙂.

如果您知道自己在做什么,您还可以选择刷新整个树。该 API 称为ReactDOM.flushSync(fn). 我认为我们还没有记录它,但我们肯定会在 16.x 发布周期的某个时候这样做。请注意,它实际上强制对调用内部发生的更新进行完全重新渲染,因此您应该非常谨慎地使用它。这样就不会破坏propsstaterefs 之间的内部一致性保证。

总而言之,React 模型并不总能产生最简洁的代码,但它在内部是一致的,并确保提升状态是安全的。

启用并发更新

从概念上讲,React 的行为就好像每个组件都有一个更新队列。这就是为什么讨论完全有意义的原因:我们讨论是否this.state立即应用更新,因为我们毫不怀疑更新将以确切的顺序应用。但是,情况不一定如此。

我们一直在解释“异步渲染”的一种方式是,React 可以根据调用的来源为调用分配不同的优先级setState():事件处理程序、网络响应、动画等

例如,如果您正在键入消息,则需要立即刷新组件setState()中的调用。但是,如果您在键入时TextBox收到一条新消息,最好将新消息的呈现延迟到某个阈值(例如一秒),而不是让键入由于阻塞线程而结结巴巴。MessageBubble

如果我们让某些更新具有“较低优先级”,我们可以将它们的渲染分成几毫秒的小块,这样用户就不会注意到它们。

我知道像这样的性能优化听起来可能不太令人兴奋或令人信服。你可以说:“MobX 不需要这个,我们的更新跟踪速度足够快,可以避免重新渲染”。我不认为在所有情况下都是如此(例如,无论 MobX 有多快,您仍然必须创建 DOM 节点并为新安装的视图进行渲染)。尽管如此,如果这是真的,并且如果您有意识地决定始终将对象包装到跟踪读取和写入的特定 JavaScript 库中是可以的,那么您可能不会从这些优化中获得太多好处。

但异步渲染不仅仅是性能优化。我们认为这是 React 组件模型可以做什么的根本性转变。

例如,考虑从一个屏幕导航到另一个屏幕的情况。通常,您会在渲染新屏幕时显示一个微调器。

但是,如果导航速度足够快(大约在一秒钟内),闪烁并立即隐藏微调器会导致用户体验下降。更糟糕的是,如果您有多个级别的组件具有不同的异步依赖项(数据、代码、图像),您最终会得到一连串短暂闪烁的微调器。由于所有的 DOM 重排,这既在视觉上令人不快,又使您的应用程序在实践中变慢。它也是许多样板代码的来源。

如果当你做一个setState()渲染不同视图的简单操作时,我们可以“开始”在“后台”渲染更新后的视图,那不是很好吗?想象一下,如果您自己不编写任何协调代码,您可以选择在更新时间超过某个阈值(例如一秒)时显示微调器,否则当整个新子树的异步依赖项是时让 React 执行无缝转换满意。此外,当我们“等待”时,“旧屏幕”保持交互状态(例如,您可以选择要转换到的不同项目),并且 React 强制要求如果等待时间过长,您必须显示一个微调器。

请注意,这只是可能的,因为this.state不会立即刷新。如果它立即刷新,我们将无法开始在后台渲染视图的“新版本”,而“旧版本”仍然可见且可交互。他们独立的状态更新会发生冲突。