Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(译)为什么React的setState是异步? #19

Open
jingzhiMo opened this issue Mar 4, 2020 · 1 comment
Open

(译)为什么React的setState是异步? #19

jingzhiMo opened this issue Mar 4, 2020 · 1 comment

Comments

@jingzhiMo
Copy link
Owner

jingzhiMo commented Mar 4, 2020

前言

这篇文章是Dan Abramov 在github上面的一个issue的讨论回答,虽然并不是一个正式发的文章,但是我觉得对于理解也是很重要,能够了解到设计的原因,这样子比大部分搜索到的“复制-粘贴”资料更深入。

原文链接在这里,大家有兴趣可以去看一下原版,以下是我渣渣英语的翻译:

正文

这里有几个想法,在某种意义上,这不是一个完整的回答,但仍然比不回答任何东西有帮助。

第一点,我认为我们为了批量更新而延迟调度(reconciliation)是很有利的。我们认同setState触发同步重新渲染在很多情况下是低效的,如果我们知道我们将要执行几个任务,那么批量更新是一个更好的选择。

举个例子,如果我们在浏览器点击的回调方法中,子组件(Child)与父组件(Parent)都调用了setState,我们不想去重新渲染两次子组件(Child),而是去标识这两个组件都是脏的(dirty),然后在退出浏览器事件(click)之前,把父子组件都重新渲染。

你提出一个问题:为什么我们不能够做同样的事情(批量更新),而是在调度(reconciliation)的最后来通过setState来马上更新this.state.我想目前没有一个明确的答案(两种解决方法(指同步和异步)都有权衡),但是下面是我想到的几个原因:

保证内部一致性(Guaranteeing Internal Consistency)

尽管state的更新是同步的,但是props不是(你不知道props值,除非你重新渲染父组件;如果使用同步的方法更新这些数据(译注:propsstate),批量更新就会超出处理窗口(batching goes out of the window))。

现在React提供的对象props,state,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

然而,假设需要提升数据状态来给到几个组件之间进行共享,你需要把该操作移动到父组件:

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

我想强调的是,在React app中,app依赖的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,因为这样子(同步)就意味着我们我们就要放弃批量更新(对于一些情况来说,会明显降低表现性能)。

这里也有一些小的例子,说明同步是不能够正常运行。例如,如果你把this.props(还没更新)与this.state(提议马上更新)混合一起,创建一个新的state#122(comment),使用Refs也会有这个问题:#122(comment)

上面这些例子不是所有的理论假设。 事实上,React Redux的绑定通常明确会有这些问题,因为他们把React props与不是React的state数据混合在一起: reduxjs/react-redux#86, reduxjs/react-redux#99, reduxjs/react-redux#292, reduxjs/redux#1415, reduxjs/react-redux#525

我不知道为什么Mobx的使用者们没有碰到这个问题,但是我的直觉是,他们可能在某些情景遇到这个问题,但是他们认为是他们自己的错。或者有可能他们没有直接从props读取数据,而是直接读取MobX变化的数据。

所以现在React是怎么解决的?在React中,this.statethis.props只在调度与刷新完成后才更新。所以你将会看到,在重构完的例子中,执行前后都是打印出0。这样子可以让状态提升的state变得安全。

是的,这样子的调用可能会在某些情况不方便。特别是对于人们以 OO 为背景,仅想通过更改几次state,而不是思考怎样在一处地方去完整更新state。我对这种处理也有同感,但我认为保持集中更新state对于调试debugger过程是非常清晰的。

你仍然有其他一些方法来更改state,通过一些有副作用的可变的对象(mutable object),为了马上能够读取到state。特别是当你不想使用该数据作为渲染的源数据的时候。就如MobX让你做的那样。🙂

如果你知道你所做的目的,你也可以有方法去更新整棵树。这个API为ReactDOM.flushSync(fn)。我认为我们还没有相关的文档关于它,但我们肯定会在16.x的release中加入这个文档。需要注意的是,这个API实际上被调用的时候,在数据更改后强制重新渲染,所以你需要很谨慎的使用它。这种方法不会打破props,state,refs之间内部数据的一致性。

总结一下,React这种模式不能够总是让代码变得简洁,但是是为了在React内部保持数据一致性,还有保证状态提升变得安全。

启用并发更新(Enabling Concurrent Updates)

从概念上面讲,React的行为就好像在每个组件中有一个单一的更新队列。这就是为什么这个讨论是有意义的:我们讨论是否应该马上更新this.state,因为我们对这些更新的应用顺序毫无疑问。然而,事实上并不是这样。haha

最近我们经常讨论“异步渲染”。我认为我们在沟通这方便做得不是很好,但这是技术(R&D)的本质:你在追求一个似乎很有希望的概念,但是你只有花很多时间下去,才能够真正的了解到它的含义。

其中一个解释“异步渲染”的是:React 会在setState()的时候,根据它们的数据来源分配不同的优先级,这些数据来源有:事件回调句柄,网络相应,动画效果等。

例如,如果你在输入一个信息,setStateTextBox组件被调用的时候需要马上刷新。然而,如果你在输入的时候,在接收一个新的信息,这样子可能更好的做法是:一定程度的延迟渲染新的信息冒泡更新,而不是因为进程的阻塞导致这个输入过程变得卡顿。

如果我们让某些更新变得“低优先级”,我们可以把这些渲染分割几个小的任务,在几毫秒内执行;这样子就不会让用户察觉到。

我知道像这样子的性能优化听起来很激动人心或有说服力。你可能会说:“我们在使用MobX不需要性能优化,我们更新跟踪是能够足够快仅为了避免重新渲染”。我认为这个说法不是在所有的情况都是对的(例如:无论 MobX 有多快,你仍然需要创建DOM节点并且在一个新的视图中挂载渲染)。尽管,假设这种情况是对的,并且如果你决定,总是使用一个特殊的JavaScript库包裹着对象是没问题的,用来跟踪数据读取与写入,可能你在这些优化中没有获得收益。

但是异步渲染不只为了性能优化。我们认为这是React组件的模式能做到的根本性转变。

例如,考虑这种情景,当你从一个页面跳转到另外一个。通常是你会在新个页面中显示一个spinner。

然而,如果这个跳转是足够快的(在一秒钟左右),刷新与马上隐藏一个spinner会导致用户体验的下降。更糟糕的事,如果你有多个组件层级,这些组件有不同的异步依赖(数据,代码,图片),最终你会在很短时间内,spinner一个一个地闪烁。这种情况会让app在视觉效果变得不好,让app实际上运行变慢,因为所有的DOM都重排了(reflow)。这也会出现在很多模版代码中。

如果当你执行一个简单的setState来渲染一个不同的视图,这不是很好吗,我们能够"开始"渲染更新视图的时候是在“后台”执行?想象一下你自己没有编写任何协调(coordination)的代码,就能够选择展示一个spinner,如果这次更新需要超过了某个阈值(例如:一秒),否则当异步依赖在整个子树中已完成,React会呈现无缝的过度。而且,当我们在“等待”,旧页面还保持可响应(例如:所以你能够选择另外一个不同的元素(item)去过度),如果这次更新耗费时间很长,React强制让你显示一个spinner。

结果发现,通过现在React的模式还有一个生命周期的调整,我们实际上能够实现(上面说的更新过度)。@acdlite 在过去几周内研究这个功能,也快要发一个RFC。

需要注意的是,这是唯一的可能,因为this.state不是马上更新。如果this.state是马上更新,当目前“旧版本”也能够看到和响应的时候,我们就没有办法在后台去开始渲染一个“新版本”视图。他们那些独立的状态更新就会崩溃。

我不想从 @acdlite 中抢先发布这个内容,但是我希望这听起来有点激动。我想这仍然像蒸汽那样去不断了解这想法。或者像我们不能够真的认识到我们所做的事情。我希望我们能够在接下来几个月说服你,并且你会欣赏React这种灵活的模式。据我所知,由于不是马上刷新state,至少在某种程度上,这种灵活性是可行的。

@alimzadeh
Copy link

برای نوشتن مقالات از زبان بین المللی استفاده کنید!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants