JQuery中的Deferred-詳解和使用 jQuery1.9.1源碼分析--Callbacks對象

 

首先,爲何要使用Deferred?javascript

先來看一段AJAX的代碼:html

1     var data;
2     $.get('api/data', function(resp) {
3         data = resp.data;
4     });
5     doSomethingFancyWithData(data);

這段代碼極容易出問題,請求時間多長或者超時,將會致使咱們獲取不到data。只有把請求設置爲同步咱們纔可以等待獲取到data,才執行咱們的函數。可是這會帶來阻塞,致使用戶界面一直被凍結,對用戶體驗有很嚴重的影響。因此咱們須要使用異步編程,java

JS的異步編程有兩種方式基於事件和基於回調,react

傳統的異步編程會帶來的一些問題,jquery

1.序列化異步操做致使的問題:web

  1),延續傳遞風格Continuation Passing Style (CPS)ajax

      2),深度嵌套編程

     3),回調地獄json

2.並行異步操做的困難api

 

下面是一段序列化異步操做的代碼:

複製代碼
 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 }
View Code

 

 

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的緩存 會優雅地處理失敗狀況。 當promiserejected狀態結束的話,咱們能夠提供一個錯誤回調來測試:

$.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…
相關文章
相關標籤/搜索