网络时代,建立现代软件时其中一个很大的限制是所须要的数据每每在远程服务器上。应用程序在等待网络请求时简单地锁死是不现实(甚至不可能)的。相反,咱们必须让应用程序在等待时保持响应。。前端
为此,咱们须要写出并发的代码。当应用的某一部分正在等待网络请求的响应时,其余部分必须继续运行。 Promise 对于编写非阻塞型的代码是很不错的工具,并且你的浏览器就支持这个。promise
Promise 能让潜在可怕的异步代码变得很是友好。下面假设一个博客的文章视图这样从远程服务器加载一篇文章并显示它:浏览器
// Called from `componentWillMount` and `componentWillReceiveProps`: ArticleView.prototype.updateArticle = function (props) { this.setState({ error: null, title: null, body: null }); ArticleStore.fetch(props.articleID).then(article => { this.setState({ title: article.title, body: article.body }); }).catch(err => { this.setState({ error: 'Oh Noes!' }); }); };
注意:这个例子使用了 React,可是这个概念适用于绝大多数前端视图系统。服务器
这样的代码是很优雅的。许多复杂的异步调用消失了,取而代之的是直接明了的代码。然而,使用 promise 并不能保证代码是正确的。网络
注意到我例子中引入的不易察觉的竞态条件了吗?并发
提示:竞态条件出现的缘由是没法保证异步操做的完成会按照他们开始时一样的顺序。框架
轮子掉了异步
为了阐明竞态条件,假设有这样一个左侧是文章列表,右侧是选中的文章内容的博客:
App with Article 1 Selectedasync
让咱们从第一个选中的文章标题开始。而后,选中第二个文章标题。该应用发送一个请求去加载文章的内容(this.store.fetchArticle(2)),而且用户能够看见一个加载的指示器,就像这样:
App with Article 2 Selected函数
由于网络缘由,文章内容的加载须要一小会儿。数秒以后,用户以为厌烦就(又)选择了第一篇文章。因为这篇文章已经加载过,它的内容几乎当即显示,应用仿佛回到最开始的状态。
App with Article 1 Reselected
可是接着发生了奇怪的事情:应用最终收到了第二篇文章的内容,文章视图只好尽职地更新它的标题和主体来显示新加载的内容,致使用户看到这样的厌恶的东西:
App with Article 1 Selected but Article 2 Displayed
文章列表(也多是 URL 和其余 UI 元素)代表选中的是第一篇文章,可是用户看到的倒是第二篇文章的内容。
这个问题很严重,更糟糕的是在开发环境你未必能发现。在你的本机上(或者本局域网等等),加载更快并且更少出现意外。所以,代码运行时,在等待请求完成的过程当中你极可能不会以为厌烦。
装回轮子
首先要明白发生了什么才能解决这个问题。咱们遇到的竞态条件过程以下: 1.在状态 A 时开始异步操做(选中第二篇文章)。 2.应用变换至状态 B (选中第一篇文章)。 3.异步操做完成,然而代码仍然按应用处于状态 A 来处理。
找出问题以后,咱们就能够设计解决方案了。跟绝大多数 bug 同样,也有不少备选方案。理想的方案是从一开始就杜绝产生 bug 的可能。例如,不少路由库将 promise 做为路由选择的一部分,从而避免了此类 bug。若是你手上有这样的工具能够直接使用。
然而,在这种状况下须要咱们自行管理这些 promise。这里要杜绝产生竞态条件不大现实,因此只好退而求其次,使竞态条件简单明了的抵消。
我最喜欢的『简单明了』的方案是这样的: 1.异步操做开始时记录应用的相关状态。 2.异步操做完成后校验应用是否仍处于同一状态。
举例以下:
ArticleView.prototype.updateArticle = function (props) { this.setState({ error: null, title: null, body: null }); // 记录应用的状态: var id = props.articleID; ArticleStore.fetch(id).then(article => { // 校验应用的状态: if (this.props.articleID !== id) return; this.setState({ title: article.title, body:article.body }); }).catch(err => { // 校验应用的状态: if (this.props.articleID !== id) return; this.setState({ error: 'Oh Noes!' }); }); };
之因此喜欢这个方案是由于记录和校验状态的全部代码都在一块,正好紧挨着异步操做的代码。
结语
这个问题并非基于 promise 的代码特有的,Node 式的回调代码也有一样的问题。基于 promise 的代码看起来愈来愈无害处,尽管它能轻松避免这样的问题。虽然我很乐意使用 async 函数和 await 关键字,但有点担忧他们更容易致使忽略这些问题(这里有个例子):
我在本文中所举的例子并不是子虚乌有,它来自我在实际产品应用中看到的代码。
异步代码是开发者最难搞懂的事情之一 。执行顺序的数量会随着异步操做的数量呈指数增加,很快使代码变得很是的复杂。
若是可能,利用平台或框架级的抽象来管理所以增长的复杂性。不然,最好将异步操做当作严格的界限。(异步操做完成)代码恢复时,将一切都当成已改变,由于它也许改变了。