jQuery 中的 Deferred 和 Promises (2)
看前首先了解:Promises/A規(guī)范,具體可以看這里,http://www.css88.com/archives/4743
由于jQuery版本問題對(duì)Deferred對(duì)象的實(shí)現(xiàn)有所不同,具體請(qǐng)參照jQuery api;

jQuery.Deferred()基于Promises/A規(guī)范實(shí)現(xiàn),因?yàn)閖Query本身的設(shè)計(jì)風(fēng)格,jQuery.Deferred()并沒有完全遵循Promises/A規(guī)范。
jQuery在1.5版本中首次引入了Deferred。你可以通過jQuery.Deferred() 在未來某個(gè)時(shí)候 得到 ‘延時(shí)’返回值。 在此之前是無法單獨(dú)使用的,Deferred作為對(duì)ajax模塊較大重寫的一部分添加進(jìn)來。1.5和1.6版本包含deferred功能,可以使$.ajax() 接收調(diào)用完成及請(qǐng)求出錯(cuò)的回調(diào),但卻存在嚴(yán)重的耦合。新版本的jQuery提供了一些增強(qiáng)的方式來管理回調(diào),提供更加靈活的方式建立回調(diào),而不用關(guān)心原始的回調(diào)是否已經(jīng)觸發(fā)。 jQuery的Deferred對(duì)象支持多個(gè)回調(diào)綁定多個(gè)任務(wù),任務(wù)本身既可以是同步也可以是異步的。
舉個(gè)簡(jiǎn)單地例子,原來我們寫異步的ajax請(qǐng)求是這么寫:
從 jQuery 1.5 開始,$.ajax()返回的jqXHR對(duì)象 實(shí)現(xiàn)了 Promise 接口, 使它擁有了 Promise 的所有屬性,方法和行為。(見Deferred object獲取更多信息)。為了讓回調(diào)函數(shù)的名字統(tǒng)一,便于在$.ajax()中使用。jqXHR也提供.error() .success()和.complete()方法。所以我們可以這樣寫:
或者這樣寫:
jQuery.Deferred對(duì)象:
了解jQuery.Deferred對(duì)象可以看下面這個(gè)表格。
jQuery.Deferred() |
創(chuàng)建一個(gè)新的Deferred對(duì)象的構(gòu)造函數(shù),可以帶一個(gè)可選的函數(shù)參數(shù),它會(huì)在構(gòu)造完成后被調(diào)用。 |
jQuery.when() |
通過該方式來執(zhí)行基于一個(gè)或多個(gè)表示異步任務(wù)的對(duì)象上的回調(diào)函數(shù) |
jQuery.ajax() | 執(zhí)行異步Ajax請(qǐng)求,返回實(shí)現(xiàn)了promise接口的jqXHR對(duì)象 |
deferred.then( doneFilter [, failFilter ] [, progressFilter ] ) | 當(dāng)Deferred(延遲)對(duì)象解決,拒絕或仍在進(jìn)行中時(shí),調(diào)用添加處理程序。 |
deferred.done() | 當(dāng)延遲成功時(shí)調(diào)用一個(gè)函數(shù)或者數(shù)組函數(shù). |
deferred.fail() | 當(dāng)延遲失敗時(shí)調(diào)用一個(gè)函數(shù)或者數(shù)組函數(shù).。 |
deferred.always() | 當(dāng)Deferred(延遲)對(duì)象解決或拒絕時(shí),調(diào)用添加處理程序。 |
deferred.resolve(ARG1,ARG2,…) | 調(diào)用Deferred對(duì)象注冊(cè)的‘done’回調(diào)函數(shù)并傳遞參數(shù) |
deferred.resolveWith(context,args) | 調(diào)用Deferred對(duì)象注冊(cè)的‘done’回調(diào)函數(shù)并傳遞參數(shù)和設(shè)置回調(diào)上下文 |
deferred.isResolved | 確定一個(gè)Deferred對(duì)象是否已經(jīng)解決。 |
deferred.reject(arg1,arg2,…) | 調(diào)用Deferred對(duì)象注冊(cè)的‘fail’回調(diào)函數(shù)并傳遞參數(shù) |
deferred.rejectWith(context,args) | 調(diào)用Deferred對(duì)象注冊(cè)的‘fail’回調(diào)函數(shù)并傳遞參數(shù)和設(shè)置回調(diào)上下文 |
deferred.promise() | 返回promise對(duì)象,這是一個(gè)偽造的deferred對(duì)象:它基于deferred并且不能改變狀態(tài)所以可以被安全的傳遞 |
具體查看API http://www.css88.com/jqapi-1.9/category/deferred-object/;
jQuery延時(shí)實(shí)現(xiàn)的核心是jQuery.Deferred:一個(gè)可以鏈?zhǔn)秸{(diào)用的構(gòu)造函數(shù)。…… 需要注意的是任何deferred對(duì)象的默認(rèn)狀態(tài)是unresolved, 回調(diào)會(huì)通過 .then() 、?.fail()等方法添加到隊(duì)列,并在稍后的過程中被執(zhí)行。
下面這張表格列舉了,哪些事件會(huì)觸發(fā)哪些回調(diào):
Callbacks\ Events→ ↓ |
deferred.always() | deferred.done() | deferred.fail() | deferred.progress() |
deferred.notify() | 觸發(fā) | |||
deferred.notifyWith() | 觸發(fā) | |||
deferred.reject() | 觸發(fā) | 觸發(fā) | ||
deferred.rejectWith() | 觸發(fā) | 觸發(fā) | ||
deferred.resolve() | 觸發(fā) | 觸發(fā) | ||
deferred.resolveWith() | 觸發(fā) | 觸發(fā) |
靈活的?jQuery.Deferred對(duì)象
$.deferred()方法
作用是生成一個(gè)deferred對(duì)象。
var deferred = $.deferred();
done() 和 fail()
這兩個(gè)方法都用來綁定回調(diào)函數(shù)。done()指定非同步操作成功后的回調(diào)函數(shù),fail()指定失敗后的回調(diào)函數(shù)。
var deferred = $.Deferred(); deferred.done(function(value) { alert(value); });
它們返回的是原有的deferred對(duì)象,因此可以采用鏈?zhǔn)綄懛ǎ诤竺嬖冁溄觿e的方法(包括done和fail在內(nèi))。
resolve() 和 reject()
這兩個(gè)方法用來改變deferred對(duì)象的狀態(tài)。resolve()將狀態(tài)改為非同步操作成功,reject()改為操作失敗。
var deferred = $.Deferred(); deferred.done(function(value) { alert(value); }); deferred.resolve("hello world");
一旦調(diào)用resolve(),就會(huì)依次執(zhí)行done()和then()方法指定的回調(diào)函數(shù);一旦調(diào)用reject(),就會(huì)依次執(zhí)行fail()和then()方法指定的回調(diào)函數(shù)。
state方法
該方法用來返回deferred對(duì)象目前的狀態(tài)。
var deferred = new $.Deferred(); deferred.state(); // "pending" deferred.resolve(); deferred.state(); // "resolved"
該方法的返回值有三個(gè):
- pending:表示操作還沒有完成。
- resolved:表示操作成功。
- rejected:表示操作失敗。
notify() 和 progress()
progress()用來指定一個(gè)回調(diào)函數(shù),當(dāng)調(diào)用notify()方法時(shí),該回調(diào)函數(shù)將執(zhí)行。它的用意是提供一個(gè)接口,使得在非同步操作執(zhí)行過程中,可以執(zhí)行某些操作,比如定期返回進(jìn)度條的進(jìn)度。
var userProgress = $.Deferred(); var $profileFields = $("input"); var totalFields = $profileFields.length userProgress.progress(function (filledFields) { var pctComplete = (filledFields/totalFields)*100; $("#progress").html(pctComplete.toFixed(0)); }); userProgress.done(function () { $("#thanks").html("Thanks for completing your profile!").show(); }); $("input").on("change", function () { var filledFields = $profileFields.filter("[value!='']").length; userProgress.notify(filledFields); if (filledFields == totalFields) { userProgress.resolve(); } });
then()
then()的作用也是指定回調(diào)函數(shù),它可以接受三個(gè)參數(shù),也就是三個(gè)回調(diào)函數(shù)。第一個(gè)參數(shù)是resolve時(shí)調(diào)用的回調(diào)函數(shù),第二個(gè)參數(shù)是reject時(shí)調(diào)用的回調(diào)函數(shù),第三個(gè)參數(shù)是progress()方法調(diào)用的回調(diào)函數(shù)。
deferred.then( doneFilter [, failFilter ] [, progressFilter ] )
在jQuery 1.8之前,then()只是.done().fail()寫法的語法糖,兩種寫法是等價(jià)的。在jQuery 1.8之后,then()返回一個(gè)新的deferred對(duì)象,而done()返回的是原有的deferred對(duì)象。如果then()指定的回調(diào)函數(shù)有返回值,該返回值會(huì)作為參數(shù),傳入后面的回調(diào)函數(shù)。
var defer = jQuery.Deferred(); defer.done(function(a,b){ return a * b; }).done(function( result ) { console.log("result = " + result); }).then(function( a, b ) { return a * b; }).done(function( result ) { console.log("result = " + result); }).then(function( a, b ) { return a * b; }).done(function( result ) { console.log("result = " + result); }); defer.resolve( 2, 3 );
在jQuery 1.8版本之前,上面代碼的結(jié)果是:
result = 2 result = 2 result = 2
在jQuery 1.8版本之后,返回結(jié)果是
result = 2 result = 6 result = NaN
這一點(diǎn)需要特別引起注意。
$.ajax( url1, { dataType: "json" } ) .then(function( data ) { return $.ajax( url2, { data: { user: data.userId } } ); }).done(function( data ) { // 從url2獲取的數(shù)據(jù) });
上面代碼最后那個(gè)done方法,處理的是從url2獲取的數(shù)據(jù),而不是從url1獲取的數(shù)據(jù)。
利用then()會(huì)修改返回值這個(gè)特性,我們可以在調(diào)用其他回調(diào)函數(shù)之前,對(duì)前一步操作返回的值進(jìn)行處理。
var post = $.post("/echo/json/") .then(function(p){ return p.firstName; }); post.done(function(r){ console.log(r); });
上面代碼先使用then()方法,從返回的數(shù)據(jù)中取出所需要的字段(firstName),所以后面的操作就可以只處理這個(gè)字段了。
有時(shí),Ajax操作返回json字符串里面有一個(gè)error屬性,表示發(fā)生錯(cuò)誤。這個(gè)時(shí)候,傳統(tǒng)的方法只能是通過done()來判斷是否發(fā)生錯(cuò)誤。通過then()方法,可以讓deferred對(duì)象調(diào)用fail()方法。
var myDeferred = $.post('/echo/json/', {json:JSON.stringify({'error':true})}) .then(function (response) { if (response.error) { return $.Deferred().reject(response); } return response; },function () { return $.Deferred().reject({error:true}); } ); myDeferred.done(function (response) { $("#status").html("Success!"); }).fail(function (response) { $("#status").html("An error occurred"); });
always()
always()也是指定回調(diào)函數(shù),不管是resolve或reject都要調(diào)用。
pipe方法
pipe方法接受一個(gè)函數(shù)作為參數(shù),表示在調(diào)用then方法、done方法、fail方法、always方法指定的回調(diào)函數(shù)之前,先運(yùn)行pipe方法指定的回調(diào)函數(shù)。它通常用來對(duì)服務(wù)器返回的數(shù)據(jù)做初步處理。
promise對(duì)象
大多數(shù)情況下,我們不想讓用戶從外部更改deferred對(duì)象的狀態(tài)。這時(shí),你可以在deferred對(duì)象的基礎(chǔ)上,返回一個(gè)針對(duì)它的promise對(duì)象。我們可以把后者理解成,promise是deferred的只讀版,或者更通俗地理解成promise是一個(gè)對(duì)將要完成的任務(wù)的承諾。
你可以通過promise對(duì)象,為原始的deferred對(duì)象添加回調(diào)函數(shù),查詢它的狀態(tài),但是無法改變它的狀態(tài),也就是說promise對(duì)象不允許你調(diào)用resolve和reject方法。
function getPromise(){ return $.Deferred().promise(); } try{ getPromise().resolve("a"); } catch(err) { console.log(err); }
上面的代碼會(huì)出錯(cuò),顯示TypeError {} 。
jQuery的ajax() 方法返回的就是一個(gè)promise對(duì)象。此外,Animation類操作也可以使用promise對(duì)象。
var promise = $('div.alert').fadeIn().promise();
$.when()方法
$.when()接受多個(gè)deferred對(duì)象作為參數(shù),當(dāng)它們?nèi)窟\(yùn)行成功后,才調(diào)用resolved狀態(tài)的回調(diào)函數(shù),但只要其中有一個(gè)失敗,就調(diào)用rejected狀態(tài)的回調(diào)函數(shù)。它相當(dāng)于將多個(gè)非同步操作,合并成一個(gè)。
$.when( $.ajax( "/main.php" ), $.ajax( "/modules.php" ), $.ajax( "/lists.php" ) ).then(successFunc, failureFunc);
上面代碼表示,要等到三個(gè)ajax操作都結(jié)束以后,才執(zhí)行then方法指定的回調(diào)函數(shù)。
when方法里面要執(zhí)行多少個(gè)操作,回調(diào)函數(shù)就有多少個(gè)參數(shù),對(duì)應(yīng)前面每一個(gè)操作的返回結(jié)果。
$.when( $.ajax( "/main.php" ), $.ajax( "/modules.php" ), $.ajax( "/lists.php" ) ).then(function (resp1, resp2, resp3){ console.log(resp1); console.log(resp2); console.log(resp3); });
上面代碼的回調(diào)函數(shù)有三個(gè)參數(shù),resp1、resp2和resp3,依次對(duì)應(yīng)前面三個(gè)ajax操作的返回結(jié)果。
when方法的另一個(gè)作用是,如果它的參數(shù)返回的不是一個(gè)Deferred或Promise對(duì)象,那么when方法的回調(diào)函數(shù)將 立即運(yùn)行。
$.when({testing: 123}).done(function (x){ console.log(x.testing); // "123" });
上面代碼中指定的回調(diào)函數(shù),將在when方法后面立即運(yùn)行。
利用這個(gè)特點(diǎn),我們可以寫一個(gè)具有緩存效果的異步操作函數(shù)。也就是說,第一次調(diào)用這個(gè)函數(shù)的時(shí)候,將執(zhí)行異步操作,后面再調(diào)用這個(gè)函數(shù),將會(huì)返回緩存的結(jié)果。
function maybeAsync( num ) { var dfd = $.Deferred(); if ( num === 1 ) { setTimeout(function() { dfd.resolve( num ); }, 100); return dfd.promise(); } return num; } $.when(maybeAsync(1)).then(function (resp){ $('#target').append('<p>' + resp + '</p>'); }); $.when(maybeAsync(0)).then(function (resp){ $('#target').append( '<p>' + resp + '</p>'); });
上面代碼表示,如果maybeAsync函數(shù)的參數(shù)為1,則執(zhí)行異步操作,否則立即返回緩存的結(jié)果。
實(shí)例
wait方法
我們可以用deferred對(duì)象寫一個(gè)wait方法,表示等待多少毫秒后再執(zhí)行。
$.wait = function(time) { return $.Deferred(function(dfd) { setTimeout(dfd.resolve, time); }); }
使用方法如下:
$.wait(5000).then(function() { alert("Hello from the future!"); });
改寫setTimeout方法
在上面的wait方法的基礎(chǔ)上,還可以改寫setTimeout方法,讓其返回一個(gè)deferred對(duì)象。
function doSomethingLater(fn, time) { var dfd = $.Deferred(); setTimeout(function() { dfd.resolve(fn()); }, time || 0); return dfd.promise(); } var promise = doSomethingLater(function (){ console.log( '已經(jīng)延遲執(zhí)行' ); }, 100);
自定義操作使用deferred接口
我們可以利用deferred接口,使得任意操作都可以用done()和fail()指定回調(diào)函數(shù)。
Twitter = { search:function(query) { var dfr = $.Deferred(); $.ajax({ url:"http://search.twitter.com/search.json", data:{q:query}, dataType:'jsonp', success:dfr.resolve }); return dfr.promise(); } }
使用方法如下:
Twitter.search('intridea').then(function(data) { alert(data.results[0].text); });
deferred對(duì)象的另一個(gè)優(yōu)勢(shì)是可以附加多個(gè)回調(diào)函數(shù)。
function doSomething(arg) { var dfr = $.Deferred(); setTimeout(function() { dfr.reject("Sorry, something went wrong."); }); return dfr; } doSomething("uh oh").done(function() { alert("Won't happen, we're erroring here!"); }).fail(function(message) { alert(message) });