详解JavaScript异步编程中jQuery的promise对象的作用


Posted in Javascript onMay 03, 2016

Promise, 中文可以理解为愿望,代表单个操作完成的最终结果。一个Promise拥有三种状态:分别是unfulfilled(未满足的)、fulfilled(满足的)、failed(失败的),fulfilled状态和failed状态都可以被监听。一个愿望可以从未满足状态变为满足或者失败状态,一旦一个愿望处于满足或者失败状态,其状态将不可再变化。这种“不可改变”的特性对于一个Promise来说非常的重要,它可以避免Promise的状态监听器修改一个Promise的状态导致别的监听器的行为异常。例如:一个监听fulfilled状态的监听器把Promise的状态修改为failed,那么将触发failed状态的监听器,而如果一个failed状态监听器又把Promise的状态设置为fulfilled,那么又将触发fulfilled状态的监听器,这样将导致死循环。另外一种理解Promise这种特性的方式是把Promise看成是javascript中的primative类型的变量,这种变量可以被传入被调用的函数中,但是不可以被调用函数所改变。

每一个Promise对象都有一个方法:then(fulfilledHandler, errorHandler, progressHandler),用于监听一个Promise的不同状态。fulfilledHandler用于监听fulfilled事件,errorHandler用于监听failed事件,progressHandler用于监听progress事件。一个Promise不强制实现progress状态的事件监听(jQuery的Deferred就是一个Promise的实现,但没有实现对progress状态事件的处理)。

then(...)函数中的fulfilledHandler和errorHandler的返回值是一个新的Promise对象, 以便能够链式调用then(...)函数。每一个回调函数在正常情况下返回的是处于fulfilled状态的Promise,如果该回调函数返回错误值,那么返回的Promise状态将会变为failed。

promise在异步编程中的作用

异步模式在web编程中变得越来越重要,对于web主流语言Javascript来说,这种模式实现起来不是很利索,为此,许多Javascript库(比如 jQuery和Dojo)添加了一种称为promise的抽象(有时也称之为deferred)。通过这些库,开发人员能够在实际编程中使用 promise模式。
随着Web 2.0技术的深入,浏览器端承受了越来越多的计算压力,所以“并发”具有积极的意义。对于开发人员来说,既要保持页面与用户的交互不受影响,又要协调页面与异步任务的关系,这种非线性执行的编程要求存在适应的困难。先抛开页面交互不谈,我们能够想到对于异步调用需要处理两种结果——成功操作和失败处理。在成功的调用后,我们可能需要把返回的结果用在另一个Ajax请求中,这就会出现“函数连环套”的情况。这种情况会造成编程的复杂性。看看下面的代码示例(基于XMLHttpRequest2):

function searchTwitter(term, onload, onerror) {
 
   var xhr, results, url;
   url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
   xhr = new XMLHttpRequest();
   xhr.open('GET', url, true);
 
   xhr.onload = function (e) {
     if (this.status === 200) {
       results = JSON.parse(this.responseText);
       onload(results);
     }
   };
 
   xhr.onerror = function (e) {
     onerror(e);
   };
 
   xhr.send();
 }
 
 function handleError(error) {
   /* handle the error */
 }
 
 function concatResults() {
   /* order tweets by date */
 }
 
 function loadTweets() {
   var container = document.getElementById('container');
 
   searchTwitter('#IE10', function (data1) {
     searchTwitter('#IE9', function (data2) {
       /* Reshuffle due to date */
       var totalResults = concatResults(data1.results, data2.results);
       totalResults.forEach(function (tweet) {
         var el = document.createElement('li');
         el.innerText = tweet.text;
         container.appendChild(el);
       });
     }, handleError);
   }, handleError);
 }

上面的代码其功能是获取Twitter中hashtag为IE10和IE9的内容并在页面中显示出来。这种嵌套的回调函数难以理解,开发人员需要仔细分析哪些代码用于应用的业务逻辑,而哪些代码处理异步函数调用的,代码结构支离破碎。错误处理也分解了,我们需要在各个地方检测错误的发生并作出相应的处理。

为了降低异步编程的复杂性,开发人员一直寻找简便的方法来处理异步操作。其中一种处理模式称为promise,它代表了一种可能会长时间运行而且不一定必须完整的操作的结果。这种模式不会阻塞和等待长时间的操作完成,而是返回一个代表了承诺的(promised)结果的对象。

考虑这样一个例子,页面代码需要访问第三方的API,网络延迟可能会造成响应时间较长,在这种情况下,采用异步编程不会影响整个页面与用户的交互。promise模式通常会实现一种称为then的方法,用来注册状态变化时对应的回调函数。比如下面的代码示例:

searchTwitter(term).then(filterResults).then(displayResults);

promise模式在任何时刻都处于以下三种状态之一:未完成(unfulfilled)、已完成(resolved)和拒绝(rejected)。以CommonJS Promise/A 标准为例,promise对象上的then方法负责添加针对已完成和拒绝状态下的处理函数。then方法会返回另一个promise对象,以便于形成promise管道,这种返回promise对象的方式能够支持开发人员把异步操作串联起来,如then(resolvedHandler, rejectedHandler); 。resolvedHandler 回调函数在promise对象进入完成状态时会触发,并传递结果;rejectedHandler函数会在拒绝状态下调用。

有了promise模式,我们可以重新实现上面的Twitter示例。为了更好的理解实现方法,我们尝试着从零开始构建一个promise模式的框架。首先需要一些对象来存储promise。

var Promise = function () {
    /* initialize promise */
  };

接下来,定义then方法,接受两个参数用于处理完成和拒绝状态。

Promise.prototype.then = function (onResolved, onRejected) {
   /* invoke handlers based upon state transition */
 };

同时还需要两个方法来执行理从未完成到已完成和从未完成到拒绝的状态转变。

Promise.prototype.resolve = function (value) {
   /* move from unfulfilled to resolved */
 };
 
 Promise.prototype.reject = function (error) {
   /* move from unfulfilled to rejected */
 };

现在搭建了一个promise的架子,我们可以继续上面的示例,假设只获取IE10的内容。创建一个方法来发送Ajax请求并将其封装在promise中。这个promise对象分别在xhr.onload和xhr.onerror中指定了完成和拒绝状态的转变过程,请注意searchTwitter函数返回的正是promise对象。然后,在loadTweets中,使用then方法设置完成和拒绝状态对应的回调函数。

function searchTwitter(term) {

  var url, xhr, results, promise;
  url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
  promise = new Promise();
  xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);

  xhr.onload = function (e) {
    if (this.status === 200) {
      results = JSON.parse(this.responseText);
      promise.resolve(results);
    }
  };

  xhr.onerror = function (e) {
    promise.reject(e);
  };

  xhr.send();
  return promise;
}

function loadTweets() {
  var container = document.getElementById('container');
  searchTwitter('#IE10').then(function (data) {
    data.results.forEach(function (tweet) {
      var el = document.createElement('li');
      el.innerText = tweet.text;
      container.appendChild(el);
    });
  }, handleError);
}

到目前为止,我们可以把promise模式应用于单个Ajax请求,似乎还体现不出promise的优势来。下面来看看多个Ajax请求的并发协作。此时,我们需要另一个方法when来存储准备调用的promise对象。一旦某个promise从未完成状态转化为完成或者拒绝状态,then方法里对应的处理函数就会被调用。when方法在需要等待所有操作都完成的时候至关重要。

Promise.when = function () {
  /* handle promises arguments and queue each */
};

以刚才获取IE10和IE9两块内容的场景为例,我们可以这样来写代码:

var container, promise1, promise2;
container = document.getElementById('container');
promise1 = searchTwitter('#IE10');
promise2 = searchTwitter('#IE9');
Promise.when(promise1, promise2).then(function (data1, data2) {

  /* Reshuffle due to date */
  var totalResults = concatResults(data1.results, data2.results);
  totalResults.forEach(function (tweet) {
    var el = document.createElement('li');
    el.innerText = tweet.text;
    container.appendChild(el);
  });
}, handleError);

分析上面的代码可知,when函数会等待两个promise对象的状态发生变化再做具体的处理。在实际的Promise库中,when函数有很多变种,比如 when.some()、when.all()、when.any()等,读者从函数名字中大概能猜出几分意思来,详细的说明可以参考CommonJS的一个promise实现when.js。

除了CommonJS,其他主流的Javascript框架如jQuery、Dojo等都存在自己的promise实现。开发人员应该好好利用这种模式来降低异步编程的复杂性。我们选取Dojo为例,看一看它的实现有什么异同。

Dojo框架里实现promise模式的对象是Deferred,该对象也有then函数用于处理完成和拒绝状态并支持串联,同时还有resolve和reject,功能如之前所述。下面的代码完成了Twitter的场景:

function searchTwitter(term) {

  var url, xhr, results, def;
  url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
  def = new dojo.Deferred();
  xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);

  xhr.onload = function (e) {
    if (this.status === 200) {
      results = JSON.parse(this.responseText);
      def.resolve(results);
    }
  };

  xhr.onerror = function (e) {
    def.reject(e);
  };

  xhr.send();
  return def;
}

dojo.ready(function () {
  var container = dojo.byId('container');
  searchTwitter('#IE10').then(function (data) {
    data.results.forEach(function (tweet) {
      dojo.create('li', {
        innerHTML: tweet.text
      }, container);
    });
  });
});

不仅如此,类似dojo.xhrGet方法返回的即是dojo.Deferred对象,所以无须自己包装promise模式。

var deferred = dojo.xhrGet({
  url: "search.json",
  handleAs: "json"
});

deferred.then(function (data) {
  /* handle results */
}, function (error) {
  /* handle error */
});

除此之外,Dojo还引入了dojo.DeferredList,支持开发人员同时处理多个dojo.Deferred对象,这其实就是上面所提到的when方法的另一种表现形式。

dojo.require("dojo.DeferredList");
dojo.ready(function () {
  var container, def1, def2, defs;
  container = dojo.byId('container');
  def1 = searchTwitter('#IE10');
  def2 = searchTwitter('#IE9');

  defs = new dojo.DeferredList([def1, def2]);

  defs.then(function (data) {
    // Handle exceptions
    if (!results[0][0] || !results[1][0]) {
      dojo.create("li", {
        innerHTML: 'an error occurred'
      }, container);
      return;
    }
    var totalResults = concatResults(data[0][1].results, data[1][1].results);

    totalResults.forEach(function (tweet) {
      dojo.create("li", {
        innerHTML: tweet.text
      }, container);
    });
  });
});

上面的代码比较清楚,不再详述。

说到这里,读者可能已经对promise模式有了一个比较完整的了解,异步编程会变得越来越重要,在这种情况下,我们需要找到办法来降低复杂度,promise模式就是一个很好的例子,它的风格比较人性化,而且主流的JS框架提供了自己的实现。所以在编程实践中,开发人员应该尝试这种便捷的编程技巧。需要注意的是,promise模式的使用需要恰当地设置promise对象,在对应的事件中调用状态转换函数,并且在最后返回promise对象。

Javascript 相关文章推荐
基于jQuery图片平滑连续滚动插件
Apr 27 Javascript
input 输入框获得/失去焦点时隐藏/显示文字(jquery版)
Apr 02 Javascript
用js实现table单元格高宽调整,兼容合并单元格(兼容IE6、7、8、FF)实例
Jun 25 Javascript
用jquery中插件dialog实现弹框效果实例代码
Nov 15 Javascript
js 剪切板应用clipboardData详细解析
Dec 17 Javascript
用IE重起计算机或者关机的示例代码
Mar 10 Javascript
AngularJS HTML编译器介绍
Dec 06 Javascript
使用jQuery管理选择结果
Jan 20 Javascript
每天一篇javascript学习小结(基础知识)
Nov 10 Javascript
vue项目中引入Sass实例方法
Aug 27 Javascript
详解小程序如何改变onLoad的执行时机
Nov 01 Javascript
JavaScript实现省份城市的三级联动
Feb 11 Javascript
jQuery的promise与deferred对象在异步回调中的作用
May 03 #Javascript
JavaScript的MVVM库Vue.js入门学习笔记
May 03 #Javascript
聊一聊JavaScript作用域和作用域链
May 03 #Javascript
小白谈谈对JS原型链的理解
May 03 #Javascript
基于Bootstrap使用jQuery实现输入框组input-group的添加与删除
May 03 #Javascript
JQuery的Pager分页器实现代码
May 03 #Javascript
个人网站留言页面(前端jQuery编写、后台php读写MySQL)
May 03 #Javascript
You might like
php self,$this,const,static,->的使用
2009/10/22 PHP
PHP 读取Postgresql中的数组
2013/04/14 PHP
php分页代码学习示例分享
2014/02/20 PHP
PHP标准类(stdclass)用法示例
2016/09/28 PHP
jquery 入门教程 [翻译] 推荐
2009/08/17 Javascript
JS运行耗时操作的延时显示方法
2010/11/19 Javascript
javascript 基础篇1 什么是js 建立第一个js程序
2012/03/14 Javascript
简单的ajax连接库分享(不用jquery的ajax)
2014/01/19 Javascript
js控制浏览器全屏示例代码
2014/02/20 Javascript
jQuery解决input超多的表单提交
2015/08/10 Javascript
JavaScript实现为input与textarea自定义hover,focus效果的方法
2015/08/21 Javascript
JS中artdialog弹出框控件之提交表单思路详解
2016/04/18 Javascript
基于jquery二维码生成插件qrcode
2017/01/07 Javascript
ES6关于Promise的用法详解
2018/05/07 Javascript
详解KOA2如何手写中间件(装饰器模式)
2018/10/11 Javascript
vue开发拖拽进度条滑动组件
2019/09/21 Javascript
原生js无缝轮播插件使用详解
2020/03/09 Javascript
详解element-ui动态限定的日期范围选择器代码片段
2020/07/03 Javascript
Python 实现链表实例代码
2017/04/07 Python
python 换位密码算法的实例详解
2017/07/19 Python
python随机取list中的元素方法
2018/04/08 Python
python类中super() 的使用解析
2019/12/19 Python
python 将dicom图片转换成jpg图片的实例
2020/01/13 Python
python实现126邮箱发送邮件
2020/05/20 Python
html5 兼容IE6结构的实现代码
2012/05/14 HTML / CSS
英国在线珠宝店:The Jewel Hut
2017/03/20 全球购物
Ryderwear澳洲官网:澳大利亚高端健身训练装备品牌
2018/09/18 全球购物
澳大利亚巧克力花束和礼品网站:Tastebuds
2019/03/15 全球购物
介绍一下Java的事务处理
2012/12/07 面试题
学生请假条
2014/04/11 职场文书
《金子》教学反思
2014/04/13 职场文书
2015年公司行政后勤工作总结
2015/05/20 职场文书
个人欠条范本
2015/07/03 职场文书
小学班主任研修日志
2015/11/13 职场文书
详解Nginx启动失败的几种错误处理
2021/04/01 Servers
使用compose函数优化代码提高可读性及扩展性
2022/06/16 Javascript