首先,爲何要使用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:
如今有很多庫已經實現了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的使用場所: