React:为什么setSate是异步的?
React:为什么setSate是异步的?

React:为什么setSate是异步的?

首先,我认为,为了批量更新而接受延迟reconciliation是有益的。也就是说,如果setState()进行同步的重绘,在许多场景下将是低效的。如果我们知道将要多次更新,最好是批量更新。

举例来说,如果我们在浏览器的click处理程序中,Child和Parent都调用setState,我们一定不希望将Child重绘两次,我们宁愿先将他们标记为dirty,在退出浏览器事件之前将其一次性重绘。

你可能会问:我们可以做同样的事情(指批处理),但是无需等待协调结束,就把setState的更新立即写入this.state吗?我认为没有一个明确的答案(任何一种方案都有权衡),但是我想到了一些理由,如下所示。

保证内部一致性

尽管state同步更新了,props也不会同步更新。(直到你重新渲染了parent组件,你才能得到新的props,如果你同步的去做了,批处理就完全消失了。)

目前React提供的对象(state, props, refs)互相之间是内部一致的。这意味着如果你只使用这些对象,他们保证引用自一个完全协调好的树结构(尽管引用的是树结构的旧版本)。这为什么很重要?

当您只使用state时,如果它同步刷新(如您所建议的),此模式将工作:

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

然而,假设这个state需要被提升,以便在几个组件之间共享,所以你把它移动到一个父组件:

-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(建议立即刷新)混合数据,以创建一个新的state时。

这些例子完全不是理论上的。事实上,React Redux bindings曾经就有这种问题,因为它们将React props 与 non-React state混合在一起。

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

那么React是如何解决这个问题的呢? 在React中,this.state和this.props都是只在协调和刷新之后才更新的,因此在重构之前和之后都将看到打印0。这使state提升变得安全。

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

尽管如此,你仍然可以选择 将您想要立即读取的状态 移动到某个横向可变对象中,尤其是在不将其用作真实渲染源的情况下。这也正是MobX让你做的🙂。

如果你知道自己在做什么,还可以选择刷新整个树。这个API叫做ReactDOM.flushSync(fn)。我们应该还没有公布这个API,但我们肯定会在16.x的版本周期中公布。请注意,对于调用内部发生的更新,它其实是强制完全重新渲染了,所以你应该非常谨慎地使用它。这样它就不会破坏props、state和refs之间的内部一致性。

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

允许并发更新

从概念上讲,React表现得好像每个组件都有一个单独的更新队列。

这就是为什么讨论是有意义的:我们讨论this.state的更新是否能够立即执行,因为我们确信那些更新将按准确的顺序执行。然而,事实并非如此。

最近,关于“async rendering”我们谈论了许多。我承认我们没有很好地传达这意味着什么,但这就是研发的本质: 你追求一个概念上看起来很有前途的想法,但你只有在花了足够的时间之后才能真正理解它的含义。

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

例如,如果您正在键入一条消息,则需要立即刷新TextBox组件中的setState()调用。然而,如果您在输入时收到了一条新消息,那么将新MessageBubble的呈现延迟到某个阈值(例如一秒钟)可能比让输入由于阻塞线程而停顿更好。

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

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

但是异步呈现不仅仅是性能优化。我们认为这是React组件模型的一个根本转变。

例如,考虑从一个屏幕导航到另一个屏幕的情况。通常情况下,您会在新屏幕呈现时显示旋转。

但是,如果导航足够快(大约一秒左右),闪烁和立即隐藏旋转会导致用户体验下降。(iOS系统‘设置’的设计即是如此,当页面转换速度足够快时,取消转轮的体验更佳)

更糟糕的是,如果您有多个具有不同异步依赖关系(数据、代码、图像)的组件级别,那么您最终会得到一个接一个快速闪烁的级联旋转。这不仅在视觉上令人不快,而且会让你的应用在实践中变慢,因为所有的DOM回流。这也是‘骨架屏’出现的原因。

如果当你做一个简单的setState()来呈现一个不同的视图时,我们可以“在后台”“开始”呈现更新完毕的视图,这不是很好吗? 想象一下,如果不需要自己编写任何协调代码,您可以选择在更新时间超过某个阈值(例如一秒钟)时显示旋转器,否则,当整个新子树的异步依赖满足时,让React执行无缝转换。此外,当我们“等待”时,“旧屏幕”保持互动性(例如,你可以选择一个不同的元素来过渡),并且React强制规定,如果时间太长,你必须显示转轮。

事实证明,通过当前的React模型和对生命周期的一些调整,我们实际上可以实现它

注意,因为this.state不会立即刷新这才是可能的。如果它被立即刷新,我们将没有办法在“旧版本”仍然可见和交互的情况下开始在后台渲染“新版本”的视图。他们的独立状态更新会发生冲突。

此文观点来自 React框架开发者 Dan Abramov

https://github.com/facebook/react/issues/11527#issuecomment-360199710

发表评论

邮箱地址不会被公开。