异步JavaScript编程中的Promise使用方法


Posted in Javascript onJuly 28, 2015

异步?

我在很多地方都看到过异步(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.     肯定(fulfilled或resolved),表明该Promise的操作成功了。
  2.     否定(rejected或failed),表明该Promise的操作失败了。
  3.     等待(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的代码:

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里做比较复杂的异步任务时,它的确可以提供相当多的帮助。

Javascript 相关文章推荐
静态页面的值传递(三部曲)
Sep 25 Javascript
js下弹出窗口的变通
Apr 18 Javascript
javascript中关于break,continue的特殊用法与介绍
May 24 Javascript
JQuery自动触发事件的方法
Jun 13 Javascript
js通过keyCode值判断单击键盘上某个键,然后触发指定的事件方法
Feb 19 Javascript
JS HTML图片显示Canvas 压缩功能
Jul 21 Javascript
基于vue2.x的电商图片放大镜插件的使用
Jan 22 Javascript
jquery引入外部CDN 加载失败则引入本地jq库
May 23 jQuery
详解Vue SPA项目优化小记
Jul 03 Javascript
详细讲解如何创建, 发布自己的 Vue UI 组件库
May 29 Javascript
ES6 Promise对象概念及用法实例详解
Oct 15 Javascript
jQuery 常用特效实例小结【显示与隐藏、淡入淡出、滑动、动画等】
May 19 jQuery
使用Browserify配合jQuery进行编程的超级指南
Jul 28 #Javascript
使用AmplifyJS组件配合JavaScript进行编程的指南
Jul 28 #Javascript
JavaScript编程中的Promise使用大全
Jul 28 #Javascript
javascript+html5实现绘制圆环的方法
Jul 28 #Javascript
学习Bootstrap组件之下拉菜单
Jul 28 #Javascript
深入了解JavaScript中的Symbol的使用方法
Jul 28 #Javascript
深入理解JavaScript中的箭头函数
Jul 28 #Javascript
You might like
4.与数据库的连接
2006/10/09 PHP
3.从实例开始
2006/10/09 PHP
PHP实现生成唯一会员卡号
2015/08/24 PHP
javascript实现的动态添加表单元素input,button等(appendChild)
2007/11/24 Javascript
基于JQuery的Select选择框的华丽变身
2011/08/23 Javascript
Prototype源码浅析 String部分(一)之有关indexOf优化
2012/01/15 Javascript
JavaScript子窗口ModalDialog中操作父窗口对像
2012/12/11 Javascript
Jquery 例外被抛出且未被接住原因介绍
2013/09/04 Javascript
jquery 页眉单行信息滚动显示实现思路及代码
2014/06/26 Javascript
JS实现字符串转日期并比较大小实例分析
2015/12/09 Javascript
动态加载js、css的实例代码
2016/05/26 Javascript
在localStorage中存储对象数组并读取的方法
2016/09/24 Javascript
javascript iframe跨域详解
2016/10/26 Javascript
微信小程序 Tab页切换更新数据
2017/01/05 Javascript
Angular.js中ng-include用法及多标签页面的实现方式详解
2017/05/07 Javascript
angular之ng-template模板加载
2017/11/09 Javascript
微信小程序实现tab左右切换效果
2020/11/15 Javascript
用vue设计一个日历表
2020/12/03 Vue.js
Vue 实现一个简单的鼠标拖拽滚动效果插件
2020/12/10 Vue.js
[03:00]2014DOTA2国际邀请赛 Titan淘汰潸然泪下Ohaiyo专访
2014/07/15 DOTA
跟老齐学Python之使用Python操作数据库(1)
2014/11/25 Python
Python获取当前路径实现代码
2017/05/08 Python
Python PyQt4实现QQ抽屉效果
2018/04/20 Python
python实现图片批量压缩程序
2018/07/23 Python
python中的global关键字的使用方法
2019/08/20 Python
python二维键值数组生成转json的例子
2019/12/06 Python
Python3.7实现验证码登录方式代码实例
2020/02/14 Python
Python3自动生成MySQL数据字典的markdown文本的实现
2020/05/07 Python
python爬虫今日热榜数据到txt文件的源码
2021/02/23 Python
关于安全演讲稿
2014/05/09 职场文书
商业街策划方案
2014/05/31 职场文书
小学生表扬稿范文
2015/05/05 职场文书
建筑工程催款函
2015/06/24 职场文书
深入解析NumPy中的Broadcasting广播机制
2021/05/30 Python
Python自动化测试PO模型封装过程详解
2021/06/22 Python
Python的代理类实现,控制访问和修改属性的权限你都了解吗
2022/03/21 Python