异步JavaScript与Promise

异步?

我在不少地方都看到过 异步(Asynchronous)这个词,但在我还不是很理解这个概念的时候,却发现本身经常会被当作“已经很清楚”(* ̄ロ ̄)。

若是你也有相似的状况,不要紧,搜索一下这个词,就能够获得大体的说明。在这里,我会对JavaScript的异步作一点额外解释。

看一下这段代码:

JavaScript代码
  1. var start = new Date();
  2. setTimeout(function(){
  3.   var end = new Date();
  4.   console.log("Time elapsed: ", end - start, "ms");
  5. }, 500);
  6. while (new Date - start < 1000) {};
这段代码运行后会获得相似Time elapsed: 1013ms这样的结果。 setTimeout()所设定的在将来500ms时执行的函数,实际等了比1000ms更多的时间后才执行。

要如何解释呢?调用setTimeout()时,一个延时事件被排入队列。而后,继续执行这以后的代码,以及更后边的代码,直到没有任何代码。没有任何代 码后,JavaScript线程进入空闲,此时JavaScript执行引擎才去翻看队列,在队列中找到“应该触发”的事件,而后调用这个事件的处理器 (函数)。处理器执行完成后,又再返回到队列,而后查看下一个事件。

单线程的JavaScript,就是这样经过队列,以事件循环的形式工做的。因此,前面的代码中,是用while将执行引擎拖在代码运行期间长达1000ms,而在所有代码运行完回到队列前,任何事件都不会触发。这就是JavaScript的异步机制。

JavaScript的异步难题

JavaScript中的异步操做可能不老是简单易行的。

Ajax也许是咱们用得最多的异步操做。以jQuery为例,发起一个Ajax请求的代码通常是这样的:

JavaScript代码
  1. // Ajax请求示意代码
  2. $.ajax({
  3.   url: url,
  4.   data: dataObject,
  5.   success: function(){},
  6.   error: function(){}
  7. });
这样的写法有什么问题吗?简单来讲,不够轻便。为何必定要在发起请求的地方,就要把success和error这些回调给写好呢?假如个人回调要作不少不少的事情,是要我想起一件事情就跑回这里添加代码吗?

再好比,咱们要完成这样一件事:有4个供Ajax访问的url地址,须要先Ajax访问第1个,在第1个访问完成后,用拿到的返回数据做为参数再访问第2个,第2个访问完成后再第3个...以此到4个所有访问完成。按照这样的写法,彷佛会变成这样:

JavaScript代码
  1. $.ajax({
  2.   url: url1,
  3.   success: function(data){
  4.     $.ajax({
  5.     url: url2,
  6.     data: data,
  7.     success: function(data){
  8.       $.ajax({
  9.         //...
  10.       });
  11.     }  
  12.     });
  13.   }
  14. })
你必定会以为这种称为Pyramid of Doom(金字塔厄运)的代码看起来很糟糕。习惯了直接附加回调的写法,就可能会对这种一个传递到下一个的异步事件感到无从入手。为这些回调函数分别命名并分离存放能够在形式上减小嵌套,使代码清晰,但仍然不能解决问题。

另外一个常见的难点是,同时发送两个Ajax请求,而后要在两个请求都成功返回后再作一件接下来的事,想想若是只按前面的方式在各自的调用位置去附加回调,这是否是好像也有点难办?

适于应对这些异步操做,可让你写出更优雅代码的就是Promise。

Promise上场

Promise是什么呢?先继续之前面jQuery的Ajax请求示意代码为例,那段代码其实能够写成这个样子:
JavaScript代码
  1. var promise = $.ajax({
  2.   url: url,
  3.   data: dataObject
  4. });
  5. promise.done(function(){});
  6. promise.fail(function(){});
这和前面的Ajax请求示意代码是等效的。能够看到,Promise的加入使得代码形式发生了变化。Ajax请求就好像变量赋值同样,被“保存”了起来。这就是封装,封装将真正意义上让异步事件变得容易起来。

封装是有用的Promise对象就像是一个封装好的对异步事件的引用。想要在这个异步事件完成后作点事情?给它附加回调就能够了,无论附加多少个也没问题!

jQuery的Ajax方法会返回一个Promise对象(这是jQuery1.5重点增长的特性)。若是我有do1()、do2()两个函数要在异步事件成功完成后执行,只须要这样作:

JavaScript代码
  1. promise.done(do1);
  2. // Other code here.
  3. promise.done(do2);
这样可要自由多了,我只要保存这个Promise对象,就在写代码的任什么时候候,给它附加任意数量的回调,而不用管这个异步事件是在哪里发起的。这就是Promise的优点。

正式的介绍

Promise应对异步操做是如此有用,以致于发展为了CommonJS的一个规范,叫作 Promises/A。Promise表明的是某一操做结束后的返回值,它有3种状态:

  • 确定(fulfilled或resolved),代表该Promise的操做成功了。
  • 否认(rejected或failed),代表该Promise的操做失败了。
  • 等待(pending),尚未获得确定或者否认的结果,进行中。
此外,还有1种名义上的状态用来表示Promise的操做已经成功或失败,也就是确定和否认状态的集合,叫作 结束(settled)。Promise还具备如下重要的特性:
  • 一个Promise只能从等待状态转变为确定或否认状态一次,一旦转变为确定或否认状态,就不再会改变状态。
  • 若是在一个Promise结束(成功或失败,同前面的说明)后,添加针对成功或失败的回调,则回调函数会当即执行。
想一想Ajax操做,发起一个请求后,等待着,而后成功收到返回或出现错误(失败)。这是否和Promise至关一致?

进一步解释Promise的特性还有一个很好的例子:jQuery的$(document).ready(onReady)。其中onReady回调函数 会在DOM就绪后执行,但有趣的是,若是在执行到这句代码以前,DOM就已经就绪了,那么onReady会当即执行,没有任何延迟(也就是说,是同步 的)。

Promise示例

生成Promise

Promises/A里列出了一系列实现了Promise的JavaScript库,jQuery也在其中。下面是用jQuery生成Promise的代码:

JavaScript代码
  1. var deferred = $.Deferred();
  2. deferred.done(function(message){console.log("Done: " + message)});
  3. deferred.resolve("morin");  // Done: morin
jQuery 本身特地定义了名为Deferred的类,它实际上就是Promise。$.Deferred()方法会返回一个新生成的Promise实例。一方面,使 用deferred.done()、deferred.fail()等为它附加回调,另外一方面,调用deferred.resolve()或 deferred.reject()来确定或否认这个Promise,且能够向回调传递任意数据。

合并Promise

还记得我前文说的同时发送2个Ajax请求的难题吗?继续以jQuery为例,Promise将能够这样解决它:

JavaScript代码
  1. var promise1 = $.ajax(url1),
  2. promise2 = $.ajax(url2),
  3. promiseCombined = $.when(promise1, promise2);
  4. promiseCombined.done(onDone);
$.when() 方法能够合并多个Promise获得一个新的Promise,至关于在原多个Promise之间创建了AND(逻辑与)的关系,若是全部组成 Promise都已成功,则令合并后的Promise也成功,若是有任意一个组成Promise失败,则当即令合并后的Promise失败。

级联Promise

再继续我前文的依次执行一系列异步任务的问题。它将用到Promise最为重要的.then()方法(在Promises/A规范中,也是用“有then()方法的对象”来定义Promise的)。代码以下:

JavaScript代码
  1. var promise = $.ajax(url1);
  2. promise = promise.then(function(data){
  3.   return $.ajax(url2, data);
  4. });
  5. promise = promise.then(function(data){
  6.   return $.ajax(url3, data);
  7. });
  8. // ...
Promise 的.then()方法的完整形式是.then(onDone, onFail, onProgress),这样看上去,它像是一个一次性就能够把各类回调都附加上去的简便方法(.done()、.fail()能够不用了)。没错,你的 确能够这样使用,这是等效的。

但.then()方法还有它更为有用的功能。如同then这个单词自己的意义那样,它用来清晰地指明异步事件的先后关系:“先这个,而后(then)再那个”。这称为Promise的级联。

要级联Promise,须要注意的是,在传递给then()的回调函数中,必定要返回你想要的表明下一步任务的Promise(如上面代码 的$.ajax(url2, data))。这样,前面被赋值的那个变量才会变成新的Promise。而若是then()的回调函数返回的不是Promise,则then()方法会返 回最初的那个Promise。

应该会以为有些难理解?从代码执行的角度上说,上面这段带有多个then()的代码其实仍是被JavaScript引擎运行一遍就结束。但它就像是写好的 舞台剧的剧本同样,读过一遍后,JavaScript引擎就会在将来的时刻,依次安排演员按照剧原本演出,而演出都是异步的。then()方法就是让你能 写出异步剧本的笔。

将Promise用在基于回调函数的API

前文反复用到的$.ajax()方法会返回一个Promise对象,这其实只是jQuery特地提供的福利。实际状况是,大多数JavaScript API,包括Node.js中的原生函数,都基于回调函数,而不是基于Promise。这种状况下使用Promise会须要自行作一些加工。

这个加工其实比较简单和直接,下面是例子:
JavaScript代码
  1. var deferred = $.Deferred();
  2. setTimeout(deferred.resolve, 1000);
  3. deferred.done(onDone);
这样,将Promise的确定或否认的触发器,做为API的回调传入,就变成了Promise的处理模式了。

Promise是怎么实现出来的?

本文写Promise写到这里,你发现了全都是基于已有的实现了Promise的库。那么,若是要自行构筑一个Promise的话呢?

位列于 Promises/A的库列表第一位的 Q能够算是最符合Promises/A规范且至关直观的实现。若是你想了解如何作出一个Promise,能够参考Q提供的 设计模式解析

限于篇幅,本文只介绍Promise的应用。我会在之后单独开一篇文章来详述Promise的实现细节。

做为JavaScript后续版本的ECMAScript 6将原生提供Promise,若是你想知道它的用法,推荐阅读 JavaScript Promises: There and back again

结语

Promise这个词顽强到不适合翻译,一眼之下都会以为意义不明。不过,在JavaScript里作比较复杂的异步任务时,它的确能够提供至关多的帮助。


原文: http://segmentfault.com/blog/yardtea/1190000002526897
相关文章
相关标签/搜索