前言
今天在我的React交流群里面与朋友们一起聊面试题,突然其中一个朋友问this.setState的一套东西,其他的东西我都了解过,但是当他问为什么this.setState是异步的时候我就哑口无言了。。。于是到github上就去搜答案(有很多朋友会好奇,为什么笔者搜问题先去github上面搜而不是百度,知乎,掘金等等地方?因为github上会有官方人员的解答,其他地方的答案不可靠。。。),结果终于找到了我要的答案:
链接:https://github.com/facebook/react/issues/11527
笔者在这里做个翻译:
gaearon在2017年12月20号评论:
对不起,这是今年年底,我们在GitHub等方面有点落后,试图结束我们在假期前一直在做的所有事情。
我打算回到这个主题并讨论它。但它也是一个移动目标,因为我们目前正在研究与this.state更新方式和时间直接相关的异步React功能。我不想花很多时间写一些东西,然后不得不重写它,因为潜在的假设已经改变了。所以我想保持开放,但我还不知道什么时候我能够给出一个明确的答案。
gaearon在2018年1月25号评论:
所以这里有一些想法。无论如何,这不是一个完整的回应,但也许这比没有说什么更有帮助。
首先,我认为我们同意延迟和解以进行批量更新是有益的。也就是说,我们同意setState()在许多情况下同步重新渲染效率低下,如果我们知道我们可能会获得多个更新,那么批量更新会更好。
举例来说,如果我们在浏览器中click处理,都Child和Parent调用setState,我们不想重新渲染Child两次,而是更愿意将它们标记为脏,以及它们放在一起退出浏览器事件之前重新呈现。
你问:为什么我们不能做同样的事情(批处理),而是setState立即写更新this.state而不等待和解结束。我不认为有一个明显的答案(任何解决方案都有权衡)但这里有几个我能想到的理由。
setState是异步的原因:
一、保持内部一致性
即使state是同步更新,props也不是。(props在重新渲染父组件之前,您无法知道,如果您同步执行此操作,则批处理会离开窗口。)
眼下所提供的对象做出反应(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(); //在父母身上做同样的事情
我想强调的是,在典型的React应用程序中,依赖setState()于此的是一种最常见的特定于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(建议立即刷新)的数据以创建新状态:#122(评论)。Refs提出了同样的问题:#122(评论)。
这些例子完全不是理论上的。事实上,React Redux绑定曾经有过这种问题,因为它们将React道具与非React状态混合在一起:reduxjs / react-redux#86,reduxjs / react-redux#99,reduxjs / react-redux#292,reduxjs / redux#1415,reduxjs / react-redux#525。
我不知道为什么MobX用户没有碰到这个,但我的直觉是他们可能碰到这样的场景,但认为它们是他们自己的错。或许它们不会从中读取太多内容props,而是直接从MobX可变对象中读取。
那么React今天如何解决这个问题呢?在反应,都this.state和this.props只有和解和冲洗后更新,所以你会看到0之前和之后重构正在打印。这使升降状态更安全。
是的,在某些情况下这可能不方便。特别是对于来自更多OO背景的人来说,他们只想多次改变状态,而不是想在一个地方代表完整的状态更新。我可以理解这一点,尽管我认为从调试的角度来看,保持状态更新更加清晰:#122(评论)。
尽管如此,您仍然可以选择将要立即读取的状态移动到一些侧向可变对象中,尤其是如果您不将其用作渲染真实的来源。这几乎是MobX允许你做的事情?。
如果您知道自己在做什么,也可以选择刷新整棵树。API被调用ReactDOM.flushSync(fn)。我认为我们还没有记录它,但我们肯定会在16.x发布周期的某些时候这样做。请注意,它实际上强制完全重新呈现在调用内发生的更新,因此您应该非常谨慎地使用它。这样,它不会破坏之间内在一致性的保证props,state和refs。
总而言之,React模型并不总是导致最简洁的代码,但它在内部是一致的,并确保提升状态是安全的。
二、启动并发更新
从概念上讲,React的行为就像每个组件只有一个更新队列一样。这就是为什么讨论有意义:我们讨论是否this.state立即应用更新,因为我们毫不怀疑将按照确切的顺序应用更新。但是,不一定是这种情况(哈哈)。
最近,我们一直在谈论“异步渲染”。我承认我们在沟通这意味着什么方面没有做得很好,但这就是研发的本质:你追求的是一个看似概念上很有希望的想法,但只有在花了足够的时间之后才能真正理解它的含义。
我们一直在解释“异步呈现”的一种方式是React可以setState()根据它们的来源为调用分配不同的优先级:事件处理程序,网络响应,动画等。
例如,如果要键入消息,setState()则TextBox需要立即刷新组件中的调用。但是,如果您在键入时收到新消息,则最好将新的消息延迟MessageBubble到某个阈值(例如一秒),而不是因为阻塞线程而导致打字断断续续。
如果我们让某些更新具有“较低优先级”,我们可以将它们的渲染分成几毫秒的小块,这样它们就不会被用户注意到。
我知道这样的性能优化可能听起来不太令人兴奋或令人信服。您可以说:“我们不需要使用MobX,我们的更新跟踪速度足以避免重新渲染”。我不认为在所有情况下都是如此(例如,无论MobX有多快,您仍然需要创建DOM节点并为新安装的视图进行渲染)。但是,如果它是真的,并且如果你有意识地决定将对象总是包装到跟踪读写的特定JavaScript库中,那么也许你不会从这些优化中获益。
但异步呈现不仅仅是性能优化。我们认为这是React组件模型可以做的一个根本性转变。
例如,考虑您从一个屏幕导航到另一个屏幕的情况。通常,您会在新屏幕渲染时显示微调器。
但是,如果导航足够快(大约一秒钟左右),闪烁并立即隐藏微调器会导致用户体验降级。更糟糕的是,如果你有多个级别的组件具有不同的异步依赖关系(数据,代码,图像),你最终会得到一个简单闪烁的级联旋转器。由于所有的DOM回流,这在视觉上都是令人不愉快的并且使你的app在实践中变慢。它也是许多样板代码的来源。
如果当你做一个简单的setState()呈现不同的视图时,我们可以“开始”在后台“渲染”更新的视图,这不是很好吗?想象一下,如果没有自己编写任何协调代码,您可以选择在更新超过某个阈值(例如一秒)时显示微调器,否则当全新子树的异步依赖性为时,让React执行无缝转换。满意。此外,当我们“等待”时,“旧屏幕”保持交互状态(例如,因此您可以选择要转换到的其他项目),并且React会强制执行,如果它需要太长时间,则必须显示一个微调器。
事实证明,使用当前的React模型和对生命周期的一些调整,我们实际上可以实现这一点!@acdlite过去几周一直致力于此功能,并将尽快发布RFC。
请注意,这是唯一可能的,因为this.state不会立即刷新。如果它立即被刷新,我们将无法在后台开始渲染视图的“新版本”,而“旧版本”仍然可见并且是交互式的。他们的独立国家更新将发生冲突。
关于宣布这一切,我不想从@acdlite窃取雷声,但我希望这听起来至少有点令人兴奋。我知道这听起来仍然像是汽化器,或者我们真的不知道我们在做什么。我希望在接下来的几个月里我们能够说服你,并且你会欣赏React模型的灵活性。据我所知,至少在某种程度上,由于不立即刷新状态更新,这种灵活性是可能的。
下方的回复: