转载

JQuery的Promise详解(一):Promise基础

前言

在网站开发中,会经常处理各种事件,如mouseclick/mouseover/click等等。在DOM 0事件模型,采取的事件监听方式是 ele.onclick=handler ,这会导致一个问题,如果想用 hanlder2 来监听 click ,则 hanlder 就会被覆盖,不会执行了,因为一次只能安排一个监听函数。但在DOM 2事件模型中,使用DOM方法中的 addEventListener 来添加事件监听就能解决这个问题,它支持你为一个事件添加多个处理程序。

Ajax的调用也经历了类似的过程。

在JQuery 1.5之前,Ajax仅支持一个回调函数,但在JQuery的1.5版本中,引入了 Deferred 对象,它允许注册多个回调函数,并且能传递任何同步或异步函数的执行状态–成功或失败。简单说, Deferred 对象就是jQuery的回调函数解决方案,它解决了如何处理耗时操作的问题,对那些操作提供了更好的控制,以及统一的编程接口。

什么是Promise

Promise 对象用来进行延迟(deferred) 和异步(asynchronous ) 计算。回顾一下在JQuery 1.5之前,传统的Ajax操作写法:

  1. $.ajax({
  2. url: "/ServerResource.txt",
  3. success: successFunction,
  4. error: errorFunction
  5. });

$.ajax()操作完成后,如果使用的是低于1.5.0版本的jQuery,返回的是XHR对象,你没法进行链式操作;如果高于1.5.0版本,返回的是deferred对象,可以进行链式操作。

现在,新的写法是这样的:

  1. var promise = $.ajax({
  2. url: "/ServerResource.txt"
  3. });
  4. promise.done(successFunction);
  5. promise.fail(errorFunction);
  6. promise.always(alwaysFunction);

在jQuery 1.6之前, always() 相当于 complete() ,在 done()fail() 执行完毕之后才执行,即无论Ajax的执行结果是什么, always() 总会执行。

done()fail() , 和 always() 会返回同一个JQuery XMLHttpRequest(jqXHR)对象,所以可以进行链式调用:

  1. $.ajax( "example.php" )
  2. .done(function() { alert("success"); })
  3. .fail(function() { alert("error"); })
  4. .always(function() { alert("complete"); });

deferred对象的一大好处,就是它允许你自由添加多个回调函数。

  1. $.ajax("test.html")
  2. .done(function(){ alert("哈哈,成功了!");} )
  3. .fail(function(){ alert("出错啦!"); } )
  4. .done(function(){ alert("第二个回调函数!");} );

回调函数可以添加任意多个,它们按照添加顺序执行。如果在后续的代码中还需要利用改jqXHR对象,就必须用变量保存:

  1. var jqxhr = $.ajax( "example.php" )
  2. .done(function() { alert("success"); })
  3. .fail(function() { alert("error"); })
  4. .always(function() { alert("complete"); });
  5. // todo ...
  6. // 再次调用
  7. jqxhr.always(function() { alert("another complete"); });

另外一种产生链式调用的方式是利用Promise的 then 方法,它接受三个event handlers作为参数,在jquery 1.8之前,对于多个回调函数,有需要以数组方式传入三个参数:

  1. $.ajax({url: "/ServerResource.txt"})
  2. .then([successFunction1, successFunction2, successFunction3],
  3. [errorFunction1, errorFunction2]);
  4. //same as
  5. var jqxhr = $.ajax({
  6. url: "/ServerResource.txt"
  7. });
  8. jqxhr.done(successFunction1);
  9. jqxhr.done(successFunction2);
  10. jqxhr.done(successFunction3);
  11. jqxhr.fail(errorFunction1);
  12. jqxhr.fail(errorFunction2);

1.8版本之后, then 会返回一个新的Promise,它可以通过一个函数过滤掉Deferred对象的状态和值,用于取代不被推荐使用的 deferred.pipe() 方法。

  1. var promise = $.ajax({
  2. url: "/ServerResource.txt"
  3. });
  4. promise.then(successFunction, errorFunction);
  5. //如果不想处理某个事件类型,可以传入Null
  6. var promise = $.ajax({
  7. url: "/ServerResource.txt"
  8. });
  9. promise.then(successFunction); //no handler for the fail() event

then() 方法还能逐次调用多个方法,可以用于处理有着先后顺序或者依赖的多个Ajax请求:

  1. var promise = $.ajax("/myServerScript1");
  2. function getStuff() {
  3. return $.ajax("/myServerScript2");
  4. }
  5. promise.then(getStuff).then(function(myServerScript2Data){
  6. // Do something
  7. });

JQuery的Promise详解(一):Promise基础

为多个操作指定回调函数

上文提到过,Deferred对象允许你为多个事件指定一个回调函数,这是传统写法做不到的。请看下面的代码,它用到了一个新的方法 $.when()

  1. $.when($.ajax("test1.html"), $.ajax("test2.html"))
  2. .done(function(){ alert("哈哈,成功了!"); })
  3. .fail(function(){ alert("出错啦!"); });

这段代码的意思是,先执行两个操作 $.ajax("test1.html")$.ajax("test2.html") ,如果都成功了,就运行done()指定的回调函数;如果有一个失败或都失败了,就执行fail()指定的回调函数。

$.when()的参数只能是deferred对象,如果不是,则done会立即执行:

  1. var wait = function(){
  2. var tasks = function(){
  3. alert("执行完毕!");
  4. };
  5. setTimeout(tasks,5000);
  6. };
  7. $.when(wait())
  8. .done(function(){ alert("哈哈,成功了!"); })
  9. .fail(function(){ alert("出错啦!"); });

Promise的状态

在任何时刻,Promise只能处于三种状态之一:未完成(unfulfilled)、已完成(resolved)和已失败(resolved)。promise默认的状态是unresolved,任何处于回调队列的函数都会被执行。举个粟子,如果一个Ajax调用成功, $.resolved 会被调用,同时promise的状态转为resolved,以及任何监听 done 的回调都会被执行;相反,则 $.rejected 会被调用,同时promise的状态转为rejected,以及任何监听 fail 的回调都会被执行。

对上述的wait进行改写:

  1. var dtd = $.Deferred(); // 新建一个deferred对象
  2. var wait = function(dtd){
  3. var tasks = function(){
  4. alert("执行完毕!");
  5. dtd.resolve(); // 改变deferred对象的执行状态
  6. //dtd.reject(); 改变Deferred对象的执行状态
  7. };
  8. setTimeout(tasks,5000);
  9. return dtd;
  10. };

现在,wait()函数返回的是deferred对象,这就可以加上链式操作了。

  1. //wait()函数运行完,就会自动运行done()方法指定的回调函数。
  2. $.when(wait(dtd))
  3. .done(function(){ alert("哈哈,成功了!"); })
  4. .fail(function(){ alert("出错啦!"); });

在ajax操作中,deferred对象会根据返回结果,自动改变自身的执行状态;但是,在wait()函数中,这个执行状态必须由程序员手动指定。调用dtd.resolve(),将dtd对象的执行状态从”未完成”改为”已完成”,从而触发done()方法。调用dtd.reject(),将dtd对象的执行状态从”未完成”改为”已失败”,从而触发fail()方法。

但是这种写法是有问题的,因为dtd是一个全局对象,所以它的执行状态可以从外部改变。

  1. var dtd = $.Deferred(); // 新建一个Deferred对象
  2. var wait = function(dtd){
  3. var tasks = function(){
  4. alert("执行完毕!");
  5. dtd.resolve(); // 改变Deferred对象的执行状态
  6. };
  7. setTimeout(tasks,5000);
  8. return dtd;
  9. };
  10. $.when(wait(dtd))
  11. .done(function(){ alert("哈哈,成功了!"); })
  12. .fail(function(){ alert("出错啦!"); });
  13. dtd.resolve(); //在这里改变dtd的执行状态

我在代码的尾部加了一行dtd.resolve(),这就改变了dtd对象的执行状态,因此导致done()方法立刻执行,跳出”哈哈,成功了!”的提示框,等5秒之后再跳出”执行完毕!”的提示框。

为了避免这种情况,可以在内部建立Deferred对象:

  1. var wait = function(dtd){
  2. var dtd = $.Deferred(); // 在内部新建一个Deferred对象
  3. var tasks = function(){
  4. alert("执行完毕!");
  5. dtd.resolve(); // 改变Deferred对象的执行状态
  6. };
  7. setTimeout(tasks,5000);
  8. return dtd;
  9. };

另外一个方式是利用 deferred.promise() 方法:

The deferred.promise() method allows an asynchronous function to prevent other code from interfering with the progress or status of its internal request. The Promise exposes only the Deferred methods needed to attach additional handlers or determine the state (then, done, fail, always, pipe, progress, state and promise), but not ones that change the state (resolve, reject, notify, resolveWith, rejectWith, and notifyWith).

也就是说, deferred.promise() 只是阻止其他代码来改变这个 deferred 对象的状态。可以理解成,通过 deferred.promise() 方法返回的 deferred promise 对象,是没有 resolve ,reject, progress , resolveWith, rejectWith , progressWith 这些可以改变状态的方法,你只能使用 done, then ,fail 等方法添加 handler 或者判断状态。

  1. var dtd = $.Deferred(); //新建一个延迟对象
  2. var wait = function(dtd){
  3. var tasks = function(){
  4. alert("执行完毕!");
  5. dtd.resolve(); // 改变Deferred对象的执行状态
  6. };
  7. setTimeout(tasks,5000);
  8. return dtd.promise(); // 返回promise对象
  9. };

下一篇会说说关于Promise的单元测试。

参考文章:

Making Promises With jQuery Deferred jQuery的deferred对象详解
正文到此结束
Loading...