首先,為什么要使用Deferred?
先來看一段AJAX的代碼:

1 var data; 2 $.get('api/data', function(resp) { 3 data = resp.data; 4 }); 5 doSomethingFancyWithData(data);
這段代碼極容易出問題,請求時間多長或者超時,將會導致我們獲取不到data。只有把請求設置為同步我們才能夠等待獲取到data,才執行我們的函數。但是這會帶來阻塞,導致用戶界面一直被凍結,對用戶體驗有很嚴重的影響。所以我們需要使用異步編程,
JS的異步編程有兩種方式基於事件和基於回調,
傳統的異步編程會帶來的一些問題,
1.序列化異步操作導致的問題:
1),延續傳遞風格Continuation Passing Style (CPS)
2),深度嵌套
3),回調地獄
2.並行異步操作的困難
下面是一段序列化異步操作的代碼:

1 // Demonstrates nesting, CPS, 'callback hell' 2 $.get('api1/data', function(resp1) { 3 // Next that depended on the first response. 4 $.get('api2/data', function(resp2) { 5 // Next request that depended on the second response. 6 $.get('api3/data', function(resp3) { 7 // Next request that depended on the third response. 8 $.get(); // ... you get the idea. 9 }); 10 }); 11 });
當回調越來越多,嵌套越深,代碼可讀性就會越來越差。如果注冊了多個回調,那更是一場噩夢!
再看另一段有關並行化異步操作的代碼:

$.get('api1/data', function(resp1) { trackMe(); }); $.get('api2/data', function(resp2) { trackMe(); }); $.get('api3/data', function(resp3) { trackMe(); }); var trackedCount = 0; function trackMe() { ++trackedCount; if (trackedCount === 3) { doSomethingThatNeededAllThree(); } }
上面的代碼意思是當三個請求都成功就執行我們的函數(只執行一次),毫無疑問,這段代碼有點繁瑣,而且如果我們要添加失敗回調將會是一件很麻煩的事情。
我們需要一個更好的規范,那就是Promise規范,這里引用Aaron的一篇文章中的一段,http://www.cnblogs.com/aaronjs/p/3163786.html:
- 在我開始promise的“重點”之前,我想我應該給你一點它們如何工作的內貌。一個promise是一個對象——根據Promise/A規范——只需要一個方法:then。then方法帶有三個參數:一個成功回調,一個失敗回調,和一個前進回調(規范沒有要求包括前進回調的實現,但是很多都實現了)。一個全新的promise對象從每個then的調用中返回。
- 一個promise可以是三種狀態之一:未完成的,完成的,或者失敗的。promise以未完成的狀態開始,如果成功它將會是完成態,如果失敗將會是失敗態。當一個promise移動到完成態,所有注冊到它的成功回調將被調用,而且會將成功的結果值傳給它。另外,任何注冊到promise的成功回調,將會在它已經完成以后立即被調用。
- 同樣的事情發生在promise移動到失敗態的時候,除了它調用的是失敗回調而不是成功回調。對包含前進特性的實現來說,promise在它離開未完成狀態以前的任何時刻,都可以更新它的progress。當progress被更新,所有的前進回調(progress callbacks)會被傳遞以progress的值,並被立即調用。前進回調被以不同於成功和失敗回調的方式處理;如果你在一個progress更新已經發生以后注冊了一個前進回調,新的前進回調只會在它被注冊以后被已更新的progress調用。
- 我們不會進一步深入promise狀態是如何管理的,因為那不在規范之內,而且每個實現都有差別。在后面的例子中,你將會看到它是如何完成的,但目前這就是所有你需要知道的。
現在有不少庫已經實現了Deferred的操作,其中jQuery的Deferred就非常熱門:
先過目一下Deferred的API:
jQuery的有關Deferred的API簡介:

1 $.ajax('data/url') 2 .done(function(response, statusText, jqXHR){ 3 console.log(statusText); 4 }) 5 .fail(function(jqXHR, statusText, error){ 6 console.log(statusText); 7 }) 8 ,always(function(){ 9 console.log('I will always done.'); 10 });
1.done,fail,progress都是給回調列表添加回調,因為jQuery的Deferred內部使用了其$.Callbacks對象,並且增加了memory的標記(詳情請查看我的這篇文章jQuery1.9.1源碼分析--Callbacks對象),
所以如果我們第一次觸發了相應的回調列表的回調即調用了resolve,resolveWith,reject,rejectWith或者notify,notifyWith這些相應的方法,當我們再次給該回調列表添加回調時,就會立刻觸發該回調了,
即使用了done,fail,progress這些方法,而不需要我們手動觸發。jQuery的ajax會在請求完成后就會觸發相應的回調列表。所以我們后面的鏈式操作的注冊回調有可能是已經觸發了回調列表才添加的,所以它們就會立刻被執行。
2.always方法則是不管成功還是失敗都會執行該回調。
接下來要介紹重量級的then方法(也是pipe方法):
3.then方法會返回一個新的Deferred對象
* 如果then方法的參數是deferred對象,
* 上一鏈的舊deferred會調用[ done | fail | progress ]方法注冊回調,該回調內容是:執行then方法對應的參數回調(fnDone, fnFail, fnProgress)。
* 1)如果參數回調執行后返回的結果是一個promise對象,我們就給該promise對象相應的回調列表添加回調,該回調是觸發then方法返回的新promise對象的成功,失敗,處理中(done,fail,progress)的回調列表中的所有回調。
* 當我們再給then方法進行鏈式地添加回調操作(done,fail,progress,always,then)時,就是給新deferred對象注冊回調到相應的回調列表。
* 如果我們then參數fnDoneDefer, fnFailDefer, fnProgressDefer得到了解決,就會執行后面鏈式添加回調操作中的參數函數。
*
* 2)如果參數回調執行后返回的結果returned不是promise對象,就立刻觸發新deferred對象相應回調列表的所有回調,且回調函數的參數是先前的執行返回結果returned。
* 當我們再給then方法進行鏈式地添加回調操作(done,fail,progress,always,then)時,就會立刻觸發我們添加的相應的回調。
*
* 可以多個then連續使用,此功能相當於順序調用異步回調。

1 $.ajax({ 2 url: 't2.html', 3 dataType: 'html', 4 data: { 5 d: 4 6 } 7 }).then(function(){ 8 console.log('success'); 9 },function(){ 10 console.log('failed'); 11 }).then(function(){ 12 console.log('second'); 13 return $.ajax({ 14 url: 'jquery-1.9.1.js', 15 dataType: 'script' 16 }); 17 }, function(){ 18 console.log('second f'); 19 return $.ajax({ 20 url: 'jquery-1.9.1.js', 21 dataType: 'script' 22 }); 23 }).then(function(){ 24 console.log('success2'); 25 },function(){ 26 console.log('failed2'); 27 });
上面的代碼,如果第一個對t2.html的請求成功輸出success,就會執行second的ajax請求,接着針對該請求是成功還是失敗,執行success2或者failed2。
如果第一個失敗輸出failed,然后執行second f的ajax請求(注意和上面的不一樣),接着針對該請求是成功還是失敗,執行success2或者failed2。
理解這些對失敗處理很重要。
將我們上面序列化異步操作的代碼使用then方法改造后,代碼立馬變得扁平化了,可讀性也增強了:

1 var req1 = $.get('api1/data'); 2 var req2 = $.get('api2/data'); 3 var req3 = $.get('api3/data'); 4 5 req1.then(function(req1Data){ 6 return req2.done(otherFunc); 7 }).then(function(req2Data){ 8 return req3.done(otherFunc2); 9 }).then(function(req3Data){ 10 doneSomethingWithReq3(); 11 });
4.接着介紹$.when的方法使用,主要是對多個deferred對象進行並行化操作,當所有deferred對象都得到解決就執行后面添加的相應回調。

1 $.when( 2 $.ajax({ 3 4 url: 't2.html' 5 6 }), 7 $.ajax({ 8 url: 'jquery-1.9.1-study.js' 9 }) 10 ).then(function(FirstAjaxSuccessCallbackArgs, SecondAjaxSuccessCallbackArgs){ 11 console.log('success'); 12 }, function(){ 13 console.log('failed'); 14 });
如果有一個失敗了都會執行失敗的回調。
將我們上面並行化操作的代碼改良后:

1 $.when( 2 $.get('api1/data'), 3 $.get('api2/data'), 4 $.get('api3/data'), 5 { key: 'value' } 6 ).done();
5.promse方法是返回的一個promise對象,該對象只能添加回調或者查看狀態,但不能觸發。我們通常將該方法暴露給外層使用,而內部應該使用deferred來觸發回調。
如何使用deferred封裝異步函數
第一種:

1 function getData(){ 2 // 1) create the jQuery Deferred object that will be used 3 var deferred = $.Deferred(); 4 // ---- AJAX Call ---- // 5 var xhr = new XMLHttpRequest(); 6 xhr.open("GET","data",true); 7 8 // register the event handler 9 xhr.addEventListener('load',function(){ 10 if(xhr.status === 200){ 11 // 3.1) RESOLVE the DEFERRED (this will trigger all the done()...) 12 deferred.resolve(xhr.response); 13 }else{ 14 // 3.2) REJECT the DEFERRED (this will trigger all the fail()...) 15 deferred.reject("HTTP error: " + xhr.status); 16 } 17 },false) 18 19 // perform the work 20 xhr.send(); 21 // Note: could and should have used jQuery.ajax. 22 // Note: jQuery.ajax return Promise, but it is always a good idea to wrap it 23 // with application semantic in another Deferred/Promise 24 // ---- /AJAX Call ---- // 25 26 // 2) return the promise of this deferred 27 return deferred.promise(); 28 }
第二種方法:

1 function prepareInterface() { 2 return $.Deferred(function( dfd ) { 3 var latest = $( “.news, .reactions” ); 4 latest.slideDown( 500, dfd.resolve ); 5 latest.addClass( “active” ); 6 }).promise(); 7 }
Deferred的一些使用技巧:
1.異步緩存
以ajax請求為例,緩存機制需要確保我們的請求不管是否已經存在於緩存,只能被請求一次。 因此,為了緩存系統可以正確地處理請求,我們最終需要寫出一些邏輯來跟蹤綁定到給定url上的回調。

1 var cachedScriptPromises = {}; 2 3 $.cachedGetScript = function(url, callback){ 4 if(!cachedScriptPromises[url]) { 5 cachedScriptPromises[url] = $.Deferred(function(defer){ 6 $.getScript(url).then(defer.resolve, defer.reject); 7 }).promise(); 8 } 9 10 return cachedScriptPromises[url].done(callback); 11 };
我們為每一個url緩存一個promise對象。 如果給定的url沒有promise,我們創建一個deferred,並發出請求。 如果它已經存在我們只需要為它綁定回調。 該解決方案的一大優勢是,它會透明地處理新的和緩存過的請求。 另一個優點是一個基於deferred的緩存 會優雅地處理失敗情況。 當promise以‘rejected’狀態結束的話,我們可以提供一個錯誤回調來測試:
$.cachedGetScript( url ).then( successCallback, errorCallback );
請記住:無論請求是否緩存過,上面的代碼段都會正常運作!
通用異步緩存
為了使代碼盡可能的通用,我們建立一個緩存工廠並抽象出實際需要執行的任務

1 $.createCache = function(requestFunc){ 2 var cache = {}; 3 4 return function(key, callback){ 5 if(!cache[key]) { 6 cache[key] = $.Deferred(function(defer){ 7 requestFunc(defer, key); 8 }).promise(); 9 } 10 11 return cache[key].done(callback); 12 }; 13 }; 14 15 16 // 現在具體的請求邏輯已經抽象出來,我們可以重新寫cachedGetScript: 17 $.cachedGetScript = $.createCache(function(defer, url){ 18 $.getScript(url).then(defer.resolve, defer.reject); 19 });
我們可以使用這個通用的異步緩存很輕易的實現一些場景:
圖片加載

1 // 確保我們不加載同一個圖像兩次 2 $.loadImage = $.createCache(function(defer, url){ 3 var image = new Image(); 4 function clearUp(){ 5 image.onload = image.onerror = null; 6 } 7 defer.then(clearUp, clearUp); 8 image.onload = function(){ 9 defer.resolve(url); 10 }; 11 image.onerror = defer.reject; 12 image.src = url; 13 }); 14 15 // 無論image.png是否已經被加載,或者正在加載過程中,緩存都會正常工作。 16 $.loadImage( "my-image.png" ).done( callback1 ); 17 $.loadImage( "my-image.png" ).done( callback1 );
緩存響應數據

1 $.searchTwitter = $.createCache(function(defer, query){ 2 $.ajax({ 3 url: 'http://search.twitter.com/search.json', 4 data: {q: query}, 5 dataType: 'jsonp' 6 }).then(defer.resolve, defer.reject); 7 }); 8 9 // 在Twitter上進行搜索,同時緩存它們 10 $.searchTwitter( "jQuery Deferred", callback1 );
定時,
基於deferred的緩存並不限定於網絡請求;它也可以被用於定時目的。

1 // 新的afterDOMReady輔助方法用最少的計數器提供了domReady后的適當時機。 如果延遲已經過期,回調會被馬上執行。 2 $.afterDOMReady = (function(){ 3 var readyTime; 4 5 $(function(){ 6 readyTime = (new Date()).getTime(); 7 }); 8 9 return $.createCache(function(defer, delay){ 10 delay = delay || 0; 11 12 $(function(){ 13 var delta = (new Date()).getTime() - readyTime; 14 15 if(delta >= delay) { 16 defer.resolve(); 17 } else { 18 setTimeout(defer.resolve, delay - delta); 19 } 20 }); 21 }); 22 })();
2.同步多個動畫

1 var fadeLi1Out = $('ul > li').eq(0).animate({ 2 opacity: 0 3 }, 1000); 4 var fadeLi2In = $('ul > li').eq(1).animate({ 5 opacity: 1 6 }, 2000); 7 8 // 使用$.when()同步化不同的動畫 9 $.when(fadeLi1Out, fadeLi2In).done(function(){ 10 alert('done'); 11 });
雖然jQuery1.6以上的版本已經把deferred包裝到動畫里了,但如果我們想要手動實現,也是一件很輕松的事:

1 $.fn.animatePromise = function( prop, speed, easing, callback ) { 2 var elements = this; 3 4 return $.Deferred(function( defer ) { 5 elements.animate( prop, speed, easing, function() { 6 defer.resolve(); 7 if ( callback ) { 8 callback.apply( this, arguments ); 9 } 10 }); 11 }).promise(); 12 }; 13 14 // 我們也可以使用同樣的技巧,建立了一些輔助方法: 15 $.each([ "slideDown", "slideUp", "slideToggle", "fadeIn", "fadeOut", "fadeToggle" ], 16 function( _, name ) { 17 $.fn[ name + "Promise" ] = function( speed, easing, callback ) { 18 var elements = this; 19 return $.Deferred(function( defer ) { 20 elements[ name ]( speed, easing, function() { 21 defer.resolve(); 22 if ( callback ) { 23 callback.apply( this, arguments ); 24 } 25 }); 26 }).promise(); 27 }; 28 });
3.一次性事件
例如,您可能希望有一個按鈕,當它第一次被點擊時打開一個面板,面板打開之后,執行特定的初始化邏輯。 在處理這種情況時,通常會這樣寫代碼:

1 var buttonClicked = false; 2 $( "#myButton" ).click(function() { 3 if ( !buttonClicked ) { 4 buttonClicked = true; 5 initializeData(); 6 showPanel(); 7 } 8 });
這是一個非常耦合的解決辦法。 如果你想添加一些其他的操作,你必須編輯綁定代碼或拷貝一份。 如果你不這樣做,你唯一的選擇是測試buttonClicked。由於buttonClicked可能是false,新的代碼可能永遠不會被執行,因此你 可能會失去這個新的動作。
使用deferreds我們可以做的更好 (為簡化起見,下面的代碼將只適用於一個單一的元素和一個單一的事件類型,但它可以很容易地擴展為多個事件類型的集合):

1 $.fn.bindOnce = function(event, callback){ 2 var element = this; 3 defer = element.data('bind_once_defer_' + event); 4 5 if(!defer) { 6 defer = $.Deferred(); 7 8 function deferCallback(){ 9 element.off(event, deferCallback); 10 defer.resolveWith(this, arguments); 11 } 12 13 element.on(event, deferCallback); 14 element.data('bind_once_defer_' + event, defer); 15 } 16 17 return defer.done(callback).promise(); 18 }; 19 20 $.fn.firstClick = function( callback ) { 21 return this.bindOnce( "click", callback ); 22 }; 23 24 var openPanel = $( "#myButton" ).firstClick(); 25 openPanel.done( initializeData ); 26 openPanel.done( showPanel );
該代碼的工作原理如下:
· 檢查該元素是否已經綁定了一個給定事件的deferred對象
· 如果沒有,創建它,使它在觸發該事件的第一時間解決
· 然后在deferred上綁定給定的回調並返回promise
4.多個組合使用
單獨看以上每個例子,deferred的作用是有限的 。 然而,deferred真正的力量是把它們混合在一起。
*在第一次點擊時加載面板內容並打開面板
假如,我們有一個按鈕,可以打開一個面板,請求其內容然后淡入內容。使用我們前面定義的方法,我們可以這樣做:

1 var panel = $('#myPanel'); 2 panel.firstClick(function(){ 3 $.when( 4 $.get('panel.html'), 5 panel.slideDown() 6 ).done(function(ajaxArgs){ 7 panel.html(ajaxArgs[0]).fadeIn(); 8 }); 9 });
*在第一次點擊時載入圖像並打開面板
假如,我們已經的面板有內容,但我們只希望當第一次單擊按鈕時加載圖像並且當所有圖像加載成功后淡入圖像。HTML代碼如下:

1 <div id="myPanel"> 2 <img data-src="image1.png" /> 3 <img data-src="image2.png" /> 4 <img data-src="image3.png" /> 5 <img data-src="image4.png" /> 6 </div> 7 8 /* 9 我們使用data-src屬性描述圖片的真實路徑。 那么使用deferred來解決該用例的代碼如下: 10 */ 11 $('#myBtn').firstClick(function(){ 12 var panel = $('#myPanel'); 13 var promises = []; 14 15 $('img', panel).each(function(){ 16 var image = $(this); 17 var src = element.data('src'); 18 19 if(src) { 20 promises.push( 21 $.loadImage(src).then(function(){ 22 image.attr('src', src); 23 }, function(){ 24 image.attr('src', 'error.png'); 25 }) 26 ); 27 } 28 }); 29 30 promises.push(panel.slideDown); 31 32 $.when.apply(null, promises).done(function(){ 33 panel.fadeIn(); 34 }); 35 });
*在特定延時后加載頁面上的圖像
假如,我們要在整個頁面實現延遲圖像顯示。 要做到這一點,我們需要的HTML的格式如下:

1 <img data-src="image1.png" data-after="1000" src="placeholder.png" /> 2 <img data-src="image2.png" data-after="1000" src="placeholder.png" /> 3 <img data-src="image1.png" src="placeholder.png" /> 4 <img data-src="image2.png" data-after="2000" src="placeholder.png" /> 5 6 /* 7 意思非常簡單: 8 image1.png,第三個圖像立即顯示,一秒后第一個圖像顯示 9 image2.png 一秒鍾后顯示第二個圖像,兩秒鍾后顯示第四個圖像 10 */ 11 12 $( "img" ).each(function() { 13 var element = $( this ), 14 src = element.data( "src" ), 15 after = element.data( "after" ); 16 if ( src ) { 17 $.when( 18 $.loadImage( src ), 19 $.afterDOMReady( after ) 20 ).then(function() { 21 element.attr( "src", src ); 22 }, function() { 23 element.attr( "src", "error.png" ); 24 } ).done(function() { 25 element.fadeIn(); 26 }); 27 } 28 }); 29 30 // 如果我們想延遲加載的圖像本身,代碼會有所不同: 31 $( "img" ).each(function() { 32 var element = $( this ), 33 src = element.data( "data-src" ), 34 after = element.data( "data-after" ); 35 if ( src ) { 36 $.afterDOMReady( after, function() { 37 $.loadImage( src ).then(function() { 38 element.attr( "src", src ); 39 }, function() { 40 element.attr( "src", "error.png" ); 41 } ).done(function() { 42 element.fadeIn(); 43 }); 44 } ); 45 } 46 });
這里,我們首先在嘗試加載圖片之前等待延遲條件滿足。當你想在頁面加載時限制網絡請求的數量會非常有意義。
Deferred的使用場所:
- Ajax(XMLHttpRequest)
- Image Tag,Script Tag,iframe(原理類似)
- setTimeout/setInterval
- CSS3 Transition/Animation
- HTML5 Web Database
- postMessage
- Web Workers
- Web Sockets
- and more…
相關資源:
1.jQuery1.9.1源碼分析--Callbacks對象
2.jQuery1.9.1源碼分析--Deferred對象和Promise對象
3.Creating Responsive Applications Using jQuery D...
5.移動web app開發必備 - 異步隊列 Deferred