jQuery中的Deferred詳解和使用
首先,為什麼要使用Deferred?
先來看一段AJAX的程式碼:
var data;
$.get('api/data', function(resp) {
data = resp.data;
});
doSomethingFancyWithData(data);
這段程式碼極容易出問題,請求時間多長或者超時,將會導致我們獲取不到data。只有把請求設定為同步我們才能夠等待獲取到data
,才執行我們的函式。但是這會帶來阻塞,導致使用者介面一直被凍結,對使用者體驗有很嚴重的影響。所以我們需要使用非同步程式設計,
JS的非同步程式設計有兩種方式基於事件和基於回撥,
傳統的非同步程式設計會帶來的一些問題,
1.序列化非同步操作導致的問題:
1),延續傳遞風格Continuation Passing Style (CPS)
2),深度巢狀
3),回撥地獄
2.並行非同步操作的困難
下面是一段序列化非同步操作的程式碼:
// Demonstrates nesting, CPS, 'callback hell'
$.get('api1/data', function(resp1) {
// Next that depended on the first response.
$.get('api2/data', function(resp2) {
// Next request that depended on the second response.
$.get('api3/data', function(resp3) {
// Next request that depended on the third response.
$.get(); // ... you get the idea.
});
});
});
當回撥越來越多,巢狀越深,程式碼可讀性就會越來越差。如果註冊了多個回撥,那更是一場噩夢!
再看另一段有關並行化非同步操作的程式碼:
$.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簡介:
$.ajax('data/url')
.done(function(response, statusText, jqXHR){
console.log(statusText);
})
.fail(function(jqXHR, statusText, error){
console.log(statusText);
})
,always(function(){
console.log('I will always done.');
});
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
連續使用,此功能相當於順序呼叫非同步回撥。
$.ajax({
url: 't2.html',
dataType: 'html',
data: {
d: 4
}
}).then(function(){
console.log('success');
},function(){
console.log('failed');
}).then(function(){
console.log('second');
return $.ajax({
url: 'jquery-1.9.1.js',
dataType: 'script'
});
}, function(){
console.log('second f');
return $.ajax({
url: 'jquery-1.9.1.js',
dataType: 'script'
});
}).then(function(){
console.log('success2');
},function(){
console.log('failed2');
});
上面的程式碼,如果第一個對t2.html
的請求成功輸出success
,就會執行second
的ajax
請求,接著針對該請求是成功還是失敗,執行success2
或者failed2
。
如果第一個失敗輸出failed
,然後執行second
f
的ajax
請求(注意和上面的不一樣),接著針對該請求是成功還是失敗,執行success2
或者failed2
。
理解這些對失敗處理很重要。
將我們上面序列化非同步操作的程式碼使用then方法改造後,程式碼立馬變得扁平化了,可讀性也增強了:
var req1 = $.get('api1/data');
var req2 = $.get('api2/data');
var req3 = $.get('api3/data');
req1.then(function(req1Data){
return req2.done(otherFunc);
}).then(function(req2Data){
return req3.done(otherFunc2);
}).then(function(req3Data){
doneSomethingWithReq3();
});
4.接著介紹$.when
的方法使用,主要是對多個deferred
物件進行並行化操作,當所有deferred
物件都得到解決就執行後面新增的相應回撥。
$.when(
$.ajax({
url: 't2.html'
}),
$.ajax({
url: 'jquery-1.9.1-study.js'
})
).then(function(FirstAjaxSuccessCallbackArgs, SecondAjaxSuccessCallbackArgs){
console.log('success');
}, function(){
console.log('failed');
});
如果有一個失敗了都會執行失敗的回撥。
將我們上面並行化操作的程式碼改良後:
$.when(
$.get('api1/data'),
$.get('api2/data'),
$.get('api3/data'),
{ key: 'value' }
).done();
5.promse
方法是返回的一個promise
物件,該物件只能新增回撥或者檢視狀態,但不能觸發。我們通常將該方法暴露給外層使用,而內部應該使用deferred
來觸發回撥。
如何使用deferred
封裝非同步函式
第一種:
function getData(){
// 1) create the jQuery Deferred object that will be used
var deferred = $.Deferred();
// ---- AJAX Call ---- //
var xhr = new XMLHttpRequest();
xhr.open("GET","data",true);
// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
// 3.1) RESOLVE the DEFERRED (this will trigger all the done()...)
deferred.resolve(xhr.response);
}else{
// 3.2) REJECT the DEFERRED (this will trigger all the fail()...)
deferred.reject("HTTP error: " + xhr.status);
}
},false)
// perform the work
xhr.send();
// Note: could and should have used jQuery.ajax.
// Note: jQuery.ajax return Promise, but it is always a good idea to wrap it
// with application semantic in another Deferred/Promise
// ---- /AJAX Call ---- //
// 2) return the promise of this deferred
return deferred.promise();
}
第二種方法:
function prepareInterface() {
return $.Deferred(function( dfd ) {
var latest = $( “.news, .reactions” );
latest.slideDown( 500, dfd.resolve );
latest.addClass( “active” );
}).promise();
}
Deferred
的一些使用技巧:
1.非同步快取
以ajax
請求為例,快取機制需要確保我們的請求不管是否已經存在於快取,只能被請求一次。 因此,為了快取系統可以正確地處理請求,我們最終需要寫出一些邏輯來跟蹤繫結到給定url
上的回撥。
var cachedScriptPromises = {};
$.cachedGetScript = function(url, callback){
if(!cachedScriptPromises[url]) {
cachedScriptPromises[url] = $.Deferred(function(defer){
$.getScript(url).then(defer.resolve, defer.reject);
}).promise();
}
return cachedScriptPromises[url].done(callback);
};
我們為每一個url
快取一個promise
物件。 如果給定的url
沒有promise
,我們建立一個deferred
,併發出請求。 如果它已經存在我們只需要為它繫結回撥。 該解決方案的一大優勢是,它會透明地處理新的和快取過的請求。 另一個優點是一個基於deferred
的快取 會優雅地處理失敗情況。 當promise
以‘rejected
’狀態結束的話,我們可以提供一個錯誤回撥來測試:
$.cachedGetScript( url ).then( successCallback, errorCallback );
請記住:無論請求是否快取過,上面的程式碼段都會正常運作!
通用非同步快取
為了使程式碼儘可能的通用,我們建立一個快取工廠並抽象出實際需要執行的任務
$.createCache = function(requestFunc){
var cache = {};
return function(key, callback){
if(!cache[key]) {
cache[key] = $.Deferred(function(defer){
requestFunc(defer, key);
}).promise();
}
return cache[key].done(callback);
};
};
// 現在具體的請求邏輯已經抽象出來,我們可以重新寫cachedGetScript:
$.cachedGetScript = $.createCache(function(defer, url){
$.getScript(url).then(defer.resolve, defer.reject);
});
我們可以使用這個通用的非同步快取很輕易的實現一些場景:
圖片載入
// 確保我們不載入同一個影象兩次
$.loadImage = $.createCache(function(defer, url){
var image = new Image();
function clearUp(){
image.onload = image.onerror = null;
}
defer.then(clearUp, clearUp);
image.onload = function(){
defer.resolve(url);
};
image.onerror = defer.reject;
image.src = url;
});
// 無論image.png是否已經被載入,或者正在載入過程中,快取都會正常工作。
$.loadImage( "my-image.png" ).done( callback1 );
$.loadImage( "my-image.png" ).done( callback1 );
快取響應資料
$.searchTwitter = $.createCache(function(defer, query){
$.ajax({
url: 'http://search.twitter.com/search.json',
data: {q: query},
dataType: 'jsonp'
}).then(defer.resolve, defer.reject);
});
// 在Twitter上進行搜尋,同時快取它們
$.searchTwitter( "jQuery Deferred", callback1 );
定時,
基於deferred的快取並不限定於網路請求;它也可以被用於定時目的。
// 新的afterDOMReady輔助方法用最少的計數器提供了domReady後的適當時機。 如果延遲已經過期,回撥會被馬上執行。
$.afterDOMReady = (function(){
var readyTime;
$(function(){
readyTime = (new Date()).getTime();
});
return $.createCache(function(defer, delay){
delay = delay || 0;
$(function(){
var delta = (new Date()).getTime() - readyTime;
if(delta >= delay) {
defer.resolve();
} else {
setTimeout(defer.resolve, delay - delta);
}
});
});
})();
2.同步多個動畫
var fadeLi1Out = $('ul > li').eq(0).animate({
opacity: 0
}, 1000);
var fadeLi2In = $('ul > li').eq(1).animate({
opacity: 1
}, 2000);
// 使用$.when()同步化不同的動畫
$.when(fadeLi1Out, fadeLi2In).done(function(){
alert('done');
});
雖然jQuery1.6
以上的版本已經把deferred包裝到動畫裡了,但如果我們想要手動實現,也是一件很輕鬆的事:
$.fn.animatePromise = function( prop, speed, easing, callback ) {
var elements = this;
return $.Deferred(function( defer ) {
elements.animate( prop, speed, easing, function() {
defer.resolve();
if ( callback ) {
callback.apply( this, arguments );
}
});
}).promise();
};
// 我們也可以使用同樣的技巧,建立了一些輔助方法:
$.each([ "slideDown", "slideUp", "slideToggle", "fadeIn", "fadeOut", "fadeToggle" ],
function( _, name ) {
$.fn[ name + "Promise" ] = function( speed, easing, callback ) {
var elements = this;
return $.Deferred(function( defer ) {
elements[ name ]( speed, easing, function() {
defer.resolve();
if ( callback ) {
callback.apply( this, arguments );
}
});
}).promise();
};
});
3.一次性事件
例如,您可能希望有一個按鈕,當它第一次被點選時開啟一個皮膚,皮膚開啟之後,執行特定的初始化邏輯。 在處理這種情況時,通常會這樣寫程式碼:
var buttonClicked = false;
$( "#myButton" ).click(function() {
if ( !buttonClicked ) {
buttonClicked = true;
initializeData();
showPanel();
}
});
這是一個非常耦合的解決辦法。 如果你想新增一些其他的操作,你必須編輯繫結程式碼或拷貝一份。 如果你不這樣做,你唯一的選擇是測試buttonClicked
。由於buttonClicked
可能是false
,新的程式碼可能永遠不會被執行,因此你 可能會失去這個新的動作。
使用deferreds
我們可以做的更好 (為簡化起見,下面的程式碼將只適用於一個單一的元素和一個單一的事件型別,但它可以很容易地擴充套件為多個事件型別的集合):
$.fn.bindOnce = function(event, callback){
var element = this;
defer = element.data('bind_once_defer_' + event);
if(!defer) {
defer = $.Deferred();
function deferCallback(){
element.off(event, deferCallback);
defer.resolveWith(this, arguments);
}
element.on(event, deferCallback);
element.data('bind_once_defer_' + event, defer);
}
return defer.done(callback).promise();
};
$.fn.firstClick = function( callback ) {
return this.bindOnce( "click", callback );
};
var openPanel = $( "#myButton" ).firstClick();
openPanel.done( initializeData );
openPanel.done( showPanel );
該程式碼的工作原理如下:
· 檢查該元素是否已經繫結了一個給定事件的deferred
物件
· 如果沒有,建立它,使它在觸發該事件的第一時間解決
· 然後在deferred
上繫結給定的回撥並返回promise
4.多個組合使用
單獨看以上每個例子,deferred
的作用是有限的 。 然而,deferred
真正的力量是把它們混合在一起。
*在第一次點選時載入皮膚內容並開啟皮膚
假如,我們有一個按鈕,可以開啟一個皮膚,請求其內容然後淡入內容。使用我們前面定義的方法,我們可以這樣做:
var panel = $('#myPanel');
panel.firstClick(function(){
$.when(
$.get('panel.html'),
panel.slideDown()
).done(function(ajaxArgs){
panel.html(ajaxArgs[0]).fadeIn();
});
});
*在第一次點選時載入影象並開啟皮膚
假如,我們已經的皮膚有內容,但我們只希望當第一次單擊按鈕時載入影象並且當所有影象載入成功後淡入影象。HTML程式碼如下:
<div id="myPanel">
<img data-src="image1.png" />
<img data-src="image2.png" />
<img data-src="image3.png" />
<img data-src="image4.png" />
</div>
/*
我們使用data-src屬性描述圖片的真實路徑。 那麼使用deferred來解決該用例的程式碼如下:
*/
$('#myBtn').firstClick(function(){
var panel = $('#myPanel');
var promises = [];
$('img', panel).each(function(){
var image = $(this);
var src = element.data('src');
if(src) {
promises.push(
$.loadImage(src).then(function(){
image.attr('src', src);
}, function(){
image.attr('src', 'error.png');
})
);
}
});
promises.push(panel.slideDown);
$.when.apply(null, promises).done(function(){
panel.fadeIn();
});
});
*在特定延時後載入頁面上的影象
假如,我們要在整個頁面實現延遲影象顯示。 要做到這一點,我們需要的HTML的格式如下:
<img data-src="image1.png" data-after="1000" src="placeholder.png" />
<img data-src="image2.png" data-after="1000" src="placeholder.png" />
<img data-src="image1.png" src="placeholder.png" />
<img data-src="image2.png" data-after="2000" src="placeholder.png" />
/*
意思非常簡單:
image1.png,第三個影象立即顯示,一秒後第一個影象顯示
image2.png 一秒鐘後顯示第二個影象,兩秒鐘後顯示第四個影象
*/
$( "img" ).each(function() {
var element = $( this ),
src = element.data( "src" ),
after = element.data( "after" );
if ( src ) {
$.when(
$.loadImage( src ),
$.afterDOMReady( after )
).then(function() {
element.attr( "src", src );
}, function() {
element.attr( "src", "error.png" );
} ).done(function() {
element.fadeIn();
});
}
});
// 如果我們想延遲載入的影象本身,程式碼會有所不同:
$( "img" ).each(function() {
var element = $( this ),
src = element.data( "data-src" ),
after = element.data( "data-after" );
if ( src ) {
$.afterDOMReady( after, function() {
$.loadImage( src ).then(function() {
element.attr( "src", src );
}, function() {
element.attr( "src", "error.png" );
} ).done(function() {
element.fadeIn();
});
} );
}
});
這裡,我們首先在嘗試載入圖片之前等待延遲條件滿足。當你想在頁面載入時限制網路請求的數量會非常有意義。
Deferred
的使用場所:
Ajax(XMLHttpRequest)
Image Tag,Script Tag,iframe(原理類似)
setTimeout/setInterval
CSS3 Transition/Animation
HTML5 Web Database
postMessage
Web Workers
Web Sockets
and more…
jQuery中 $.done
和 $.always
有什麼區別呢?
jQuery中Ajax有done和always這兩個回撥方法
done
:成功時執行,異常時不會執行。
always
:不論成功與否都會執行。
相關文章
- jQuery中的Deferred-詳解和使用jQuery
- jQuery的deferred物件詳解jQuery物件
- 阮一峰:jQuery的deferred物件詳解jQuery物件
- 熟練使用使用jQuery Promise (Deferred)jQueryPromise
- jQuery DeferredjQuery
- jQuery 的deferred物件jQuery物件
- jQuery的Deferred物件概述jQuery物件
- jQuery Mobile中jQuery.mobile.changePage方法使用詳解jQuery
- deferred中done和then的區別
- jQuery Mobile中$.mobile.buttonMarkup方法使用詳解jQuery
- jQuery offset()和position()用法詳解jQuery
- jquery 中的trigge函式詳解jQuery函式
- JQuery中$.ajax()方法引數詳解jQuery
- jquery 裡的each使用方法詳解薦jQuery
- 通過 ES6 Promise 和 jQuery Deferred 的異同學習 PromisePromisejQuery
- jQuery原始碼剖析(四) - Deferred非同步回撥解決方案jQuery原始碼非同步
- Oralce 使用SQL中的exists 和not exists 用法詳解SQL
- jQuery原始碼解析Deferred非同步物件jQuery原始碼非同步物件
- RabbitMQ的詳解和使用MQ
- 通過 ES6 Promise 和 jQuery Deferred 的異同學習 Promise薦PromisejQuery
- jQuery 的語法詳解jQuery
- Jquery 中 ajaxSubmit使用講解jQueryMIT
- jquery ajax詳解jQuery
- jquery tmpl 詳解jQuery
- Oracle中job的使用詳解Oracle
- Git詳解和Github的使用Github
- jQuery上傳外掛Uploadify使用詳解jQuery
- jQuery的bind()方法用法詳解jQuery
- pycharm中安裝和使用sqlite過程詳解PyCharmSQLite
- Java 中 this 和 super 的用法詳解Java
- MyBatis中#{}和${}的區別詳解MyBatis
- PHP中的traits使用詳解PHPAI
- 詳解Android中AsyncTask的使用Android
- jQuery 事件用法詳解jQuery事件
- oracle中的exists和not exists和in用法詳解Oracle
- 【jquery】適用Deferred實現jquery將請求封裝成函式jQuery封裝函式
- maven中profiles使用詳解Maven
- jQuery根據表格欄位升序和降序詳解jQuery