这篇文章主要介绍了异步 JavaScript 编程中的 Promise 使用方法, 包含 Ajax 的结合操作等问题, 需要的朋友可以参考下
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
异步?
我在很多地方都看到过异步 (Asynchronous) 这个词,但在我还不是很理解这个概念的时候,却发现自己常常会被当做 "已经很清楚"(* ̄? ̄)。
如果你也有类似的情况,没关系,搜索一下这个词,就可以得到大致的说明。在这里,我会对 JavaScript 的异步做一点额外解释。
看一下这段代码:
- var start = new Date();
- setTimeout(function(){
- var end = new Date();
- console.log("Time elapsed: ", end - start, "ms");
- }, 500);
- while (new Date - start < 1000) {};
这段代码运行后会得到类似 Time elapsed: 1013ms 这样的结果。 setTimeout() 所设定的在未来 500ms 时执行的函数,实际等了比 1000ms 更多的时间后才执行。
要如何解释呢?调用 setTimeout() 时,一个延时事件被排入队列。然后,继续执行这之后的代码,以及更后边的代码,直到没有任何代码。没有任何代码后,JavaScript 线程进入空闲,此时 JavaScript 执行引擎才去翻看队列,在队列中找到" 应该触发 " 的事件,然后调用这个事件的处理器(函数)。处理器执行完成后,又再返回到队列,然后查看下一个事件。
单线程的 JavaScript,就是这样通过队列,以事件循环的形式工作的。所以,前面的代码中,是用 while 将执行引擎拖在代码运行期间长达 1000ms,而在全部代码运行完回到队列前,任何事件都不会触发。这就是 JavaScript 的异步机制。
JavaScript 的异步难题
JavaScript 中的异步操作可能不总是简单易行的。
Ajax 也许是我们用得最多的异步操作。以 jQuery 为例,发起一个 Ajax 请求的代码一般是这样的:
- // Ajax请求示意代码
- $.ajax({
- url: url,
- data: dataObject,
- success: function(){},
- error: function(){}
- });
这样的写法有什么问题吗?简单来说,不够轻便。为什么一定要在发起请求的地方,就要把 success 和 error 这些回调给写好呢?假如我的回调要做很多很多的事情,是要我想起一件事情就跑回这里添加代码吗?
再比如,我们要完成这样一件事:有 4 个供 Ajax 访问的 url 地址,需要先 Ajax 访问第 1 个,在第 1 个访问完成后,用拿到的返回数据作为参数再访问第 2 个,第 2 个访问完成后再第 3 个... 以此到 4 个全部访问完成。按照这样的写法,似乎会变成这样:
- $.ajax({
- url: url1,
- success: function(data){
- $.ajax({
- url: url2,
- data: data,
- success: function(data){
- $.ajax({
- //...
- });
- }
- });
- }
- })
你一定会觉得这种称为 Pyramid of Doom(金字塔厄运)的代码看起来很糟糕。习惯了直接附加回调的写法,就可能会对这种一个传递到下一个的异步事件感到无从入手。为这些回调函数分别命名并分离存放可以在形式上减少嵌套,使代码清晰,但仍然不能解决问题。
另一个常见的难点是,同时发送两个 Ajax 请求,然后要在两个请求都成功返回后再做一件接下来的事,想一想如果只按前面的方式在各自的调用位置去附加回调,这是不是好像也有点难办?
适于应对这些异步操作,可以让你写出更优雅代码的就是 Promise。
Promise 上场
Promise 是什么呢?先继续以前面 jQuery 的 Ajax 请求示意代码为例,那段代码其实可以写成这个样子:
- var promise = $.ajax({
- url: url,
- data: dataObject
- });
- promise.done(function(){});
- promise.fail(function(){});
这和前面的 Ajax 请求示意代码是等效的。可以看到,Promise 的加入使得代码形式发生了变化。Ajax 请求就好像变量赋值一样,被 "保存" 了起来。这就是封装,封装将真正意义上让异步事件变得容易起来。
封装是有用的
Promise 对象就像是一个封装好的对异步事件的引用。想要在这个异步事件完成后做点事情?给它附加回调就可以了,不管附加多少个也没问题!
jQuery 的 Ajax 方法会返回一个 Promise 对象(这是 jQuery1.5 重点增加的特性)。如果我有 do1()、do2() 两个函数要在异步事件成功完成后执行,只需要这样做:
- promise.done(do1);
- // Other code here.
- promise.done(do2);
这样可要自由多了,我只要保存这个 Promise 对象,就在写代码的任何时候,给它附加任意数量的回调,而不用管这个异步事件是在哪里发起的。这就是 Promise 的优势。
正式的介绍
Promise 应对异步操作是如此有用,以至于发展为了 CommonJS 的一个规范,叫做 Promises/A。Promise 代表的是某一操作结束后的返回值,它有 3 种状态:
此外,还有 1 种名义上的状态用来表示 Promise 的操作已经成功或失败,也就是肯定和否定状态的集合,叫做结束(settled)。Promise 还具有以下重要的特性:
想想 Ajax 操作,发起一个请求后,等待着,然后成功收到返回或出现错误(失败)。这是否和 Promise 相当一致?
进一步解释 Promise 的特性还有一个很好的例子:jQuery 的 $(document).ready(onReady)。其中 onReady 回调函数会在 DOM 就绪后执行,但有趣的是,如果在执行到这句代码之前,DOM 就已经就绪了,那么 onReady 会立即执行,没有任何延迟(也就是说,是同步的)。
Promise 示例
生成 Promise
Promises/A 里列出了一系列实现了 Promise 的 JavaScript 库,jQuery 也在其中。下面是用 jQuery 生成 Promise 的代码:
- var deferred = $.Deferred();
- deferred.done(function(message) {
- console.log("Done: " + message)
- });
- deferred.resolve("morin"); // Done: morin
jQuery 自己特意定义了名为 Deferred 的类,它实际上就是 Promise。$.Deferred() 方法会返回一个新生成的 Promise 实例。一方面,使用 deferred.done()、deferred.fail() 等为它附加回调,另一方面,调用 deferred.resolve() 或 deferred.reject() 来肯定或否定这个 Promise,且可以向回调传递任意数据。
合并 Promise
还记得我前文说的同时发送 2 个 Ajax 请求的难题吗?继续以 jQuery 为例,Promise 将可以这样解决它:
- var promise1 = $.ajax(url1),
- promise2 = $.ajax(url2),
- promiseCombined = $.when(promise1, promise2);
- promiseCombined.done(onDone);
$.when() 方法可以合并多个 Promise 得到一个新的 Promise,相当于在原多个 Promise 之间建立了 AND(逻辑与)的关系,如果所有组成 Promise 都已成功,则令合并后的 Promise 也成功,如果有任意一个组成 Promise 失败,则立即令合并后的 Promise 失败。
级联 Promise
再继续我前文的依次执行一系列异步任务的问题。它将用到 Promise 最为重要的. then() 方法(在 Promises/A 规范中,也是用" 有 then() 方法的对象" 来定义 Promise 的)。代码如下:
- var promise = $.ajax(url1);
- promise = promise.then(function(data) {
- return $.ajax(url2, data);
- });
- promise = promise.then(function(data) {
- return $.ajax(url3, data);
- });
- // ...
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 会需要自行做一些加工。
这个加工其实比较简单和直接,下面是例子:
- var deferred = $.Deferred();
- setTimeout(deferred.resolve, 1000);
- 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 里做比较复杂的异步任务时,它的确可以提供相当多的帮助。
来源: