详解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 相关文章推荐
SlideView 图片滑动(扩展/收缩)展示效果
Aug 01 Javascript
JavaScript高级程序设计 客户端存储学习笔记
Sep 10 Javascript
用js写了一个类似php的print_r输出换行功能
Feb 18 Javascript
javascript文本框内输入文字倒计数的方法
Feb 24 Javascript
javaScript事件学习小结(四)event的公共成员(属性和方法)
Jun 09 Javascript
详解vue2.0脚手架的webpack 配置文件分析
May 27 Javascript
jQuery制作全屏宽度固定高度轮播图(实例讲解)
Jul 08 jQuery
浅谈webpack 自动刷新与解析
Apr 09 Javascript
AngularJS实现的鼠标拖动画矩形框示例【可兼容IE8】
May 17 Javascript
vue+element实现表格新增、编辑、删除功能
May 28 Javascript
微信小程序实现canvas分享朋友圈海报
Jun 21 Javascript
vue 中 get / delete 传递数组参数方法
Mar 23 Vue.js
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中常用数组处理方法实例分析
2008/08/30 PHP
php设计模式 DAO(数据访问对象模式)
2011/06/26 PHP
php 指定范围内多个随机数代码实例
2016/07/18 PHP
微信小程序 消息推送php服务器验证实例详解
2017/03/30 PHP
PHP使用ActiveMQ实现消息队列的方法详解
2019/05/31 PHP
php设计模式之状态模式实例分析【星际争霸游戏案例】
2020/03/26 PHP
js option删除代码集合
2008/11/12 Javascript
JavaScript中的几个关键概念的理解-原型链的构建
2011/05/12 Javascript
自己写的Javascript计算时间差函数
2013/10/28 Javascript
js对象内部访问this修饰的成员函数示例
2014/04/27 Javascript
JS获取单击按钮单元格所在行的信息
2014/06/17 Javascript
一段非常简单的js判断浏览器的内核
2014/08/17 Javascript
Javascript实现禁止输入中文或英文的例子
2014/12/09 Javascript
JS出现失效的情况总结
2017/01/20 Javascript
vue+vux实现移动端文件上传样式
2017/07/28 Javascript
Vue组件之极简的地址选择器的实现
2018/05/31 Javascript
layui点击按钮添加可编辑的一行方法
2018/08/15 Javascript
Vue.js点击切换按钮改变内容的实例讲解
2018/08/22 Javascript
微信小程序实现swiper切换卡内嵌滚动条不显示的方法示例
2018/12/20 Javascript
微信小程序中限制激励式视频广告位显示次数(实现思路)
2019/12/06 Javascript
vuex入门最详细整理
2020/03/04 Javascript
swiperjs实现导航与tab页的联动
2020/12/13 Javascript
简单的编程0基础下Python入门指引
2015/04/01 Python
粗略分析Python中的内存泄漏
2015/04/23 Python
VPS CENTOS 上配置python,mysql,nginx,uwsgi,django的方法详解
2019/07/01 Python
Python代码块及缓存机制原理详解
2019/12/13 Python
Python安装OpenCV的示例代码
2020/03/05 Python
Python plt 利用subplot 实现在一张画布同时画多张图
2021/02/26 Python
html5手机端页面可以向右滑动导致样式受影响的问题
2018/06/20 HTML / CSS
巴西电子产品购物网站:Saldão da Informática
2018/01/09 全球购物
哈利波特商店:Harry Potter Shop
2018/11/30 全球购物
2015年度优秀员工获奖感言
2015/07/31 职场文书
银行柜员优质服务心得体会
2016/01/22 职场文书
导游词之大雁塔景区
2019/09/17 职场文书
Python合并多张图片成PDF
2021/06/09 Python
Python编程编写完善的命令行工具
2021/09/15 Python