異步JS:$.Deferred的使用css
原文連接:http://www.html5rocks.com/en/tutorials/async/deferred/html
當咱們構建一個平穩的,響應式的HTML5應用時,其中一個很是重要的方面是在不一樣部分的應用中的同步,例如數據獲取,程序處理, 動畫和用戶界面元素。html5
在桌面和原生環境之間,一個主要的區別就是瀏覽器不給訪問線程模型,但會爲用戶界面(例如DOM)提供一個單線程的訪問。這意味着全部的應用程序邏輯訪問和修改用戶界面元素老是在同一線程中,所以要保證程序的工做單位儘量的短小和高效,以及儘可能多的使用更有優點的瀏覽器提供的異步能力。git
瀏覽器異步APIsweb
很幸運,瀏覽器提供了一些異步API,例如很經常使用的XHR(XMLHttpRequest或AJAX)API,還有IndexedDB, SQLite, HTML5 Web workers和HTML5 GeoLocation (地理定位)API,甚至一些DOM相關的行爲都有異步,例如經過transitionEnd的事件的CSS3動畫。ajax
瀏覽器暴露異步編程給程序邏輯的方式是經過事件或者回調。在基於事件的異步API中,開發者給一個已存在的對象(例如HTML元素或者其餘DOM對象)註冊一個事件處理程序,而後執行它。瀏覽器一般會在不一樣的線程上表現行爲,適當的時候會在主線程中觸發事件。編程
舉個例子,寫段有段XHR API,一個基於事件的異步API,將會像這樣:promise
// Create the XHR object to do GET to /data resource 瀏覽器
var xhr = new XMLHttpRequest();緩存
xhr.open("GET","data",true);
// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
alert("We got data: " + xhr.response);
}
},false)
// perform the work
xhr.send();
另外一個基於事件的異步API CSS3 transitionEnd事件:
// get the html element with id 'flyingCar'
var flyingCarElem = document.getElementById("flyingCar");
// register an event handler
// ('transitionEnd' for FireFox, 'webkitTransitionEnd' for webkit)
flyingCarElem.addEventListener("transitionEnd",function(){
// will be called when the transition has finished.
alert("The car arrived");
});
// add the CSS3 class that will trigger the animation
// Note: some browers delegate some transitions to the GPU , but
// developer does not and should not have to care about it.
flyingCarElemen.classList.add('makeItFly')
其餘的瀏覽器API,例如SQLite和HTML5地理定位都是基於回調的。
這是一段HTML5地理定位的例子:
// call and pass the function to callback when done.
navigator.geolocation.getCurrentPosition(function(position){
alert('Lat: ' + position.coords.latitude + ' ' +
'Lon: ' + position.coords.longitude);
});
在這個例子中,咱們執行了一個方法和將一個函數做爲參數傳入,而後將會獲得請求的結果。這使得瀏覽器可以同步或異步方式實現這個功能,給一個單一的API給開發者,而無論實施細節
開始使用異步
除了瀏覽器內置的異步API,良好構建的應用程序也應該將它們的低級(基礎)的API以異步的方式暴露出來,特別是當它們要進行I/O操做或者龐大的計算處理。再舉個例子,獲取數據的API應該以異步的方式實現,而不該該像這樣:
// WRONG: this will make the UI freeze when getting the data
var data = getData();
alert("We got data: " + data);
這個API的設計須要getData()爲阻塞,但這會凍結用戶界面直到獲取到了數據。若是數據在JS上下文的內部,這個影響微乎其微,可是若是是經過網絡獲取或者在SQLite或者index存儲中,這將會對用戶體驗產生戲劇性的影響。
正確的API設計是主動地讓全部程序API可以執行一段時間,一開始就異步,由於將同步的代碼改造爲異步的代碼會是一個艱鉅的任務。
例如,簡單化的getData() API應該像這樣:
getData(function(data){
alert("We got data: " + data);
});
這樣的好處是這會讓應用程序的UI代碼一開始以異步爲中心,而後容許相關的API決定將來是否須要異步或同步。
注意,並非全部的應用程序API都須要或者應該異步。斷定準則是任何API操做I/O或者處理大型數據(超過15毫秒)都應該一開始就異步,即便第一個的實現是同步的。
失敗處理
傳統的方式是使用try/catch來處理異步編程中的錯誤,可是這種方式並非真正的奏效,由於錯誤一般發生在另外一個線程中。因此,當前執行的函數須要一個結構化的方式來通知上一級的函數當在處理過程當中發生了錯誤。
在基於事件的異步API中,當接受事件時,一般是根據應用程序代碼查詢事件或者對象來完成的。而基於回調的異步API中,最好的實踐方式是給第二個參數傳入錯誤時須要執行的函數。
咱們的getData應該像這樣:
// getData(successFunc,failFunc);
getData(function(data){
alert("We got data: " + data);
}, function(ex){
alert("oops, some problem occured: " + ex);
});
和$.Deferred結合使用
上述回調的一個限制是當咱們寫同步邏輯的時候,代碼將會變得很繁瑣。
例如,若是你須要等待兩個異步的API完成採起作第三件事時,代碼複雜度將會馬上提高:
// first do the get data.
getData(function(data){
// then get the location
getLocation(function(location){
alert("we got data: " + data + " and location: " + location);
},function(ex){
alert("getLocation failed: " + ex);
});
},function(ex){
alert("getData failed: " + ex);
});
當多個程序須要調用相同的回調時,事件會變得更加複雜。由於每次調用將必須執行這些多步驟調用,不然應用程序將不得不實施其本身的緩存機制。
很幸運,有一種相關的舊的模式叫作Promises(相似於Java的Future),jQuery核心提供了一個健壯的現代實現,$.Deferred針對異步編程提供了一個簡單而又強大的解決方案。
爲了簡便,Promises模式定義了異步API返回一個Promise對象,上一級函數得到Promise對象,而後執行done(successFunc(data)),告訴Promise對象當data解決(獲取)了,就執行successFunc這個函數。
例子:
// get the promise object for this API
var dataPromise = getData();
// register a function to get called when the data is resolved
dataPromise.done(function(data){
alert("We got data: " + data);
});
// register the failure function
dataPromise.fail(function(ex){
alert("oops, some problem occured: " + ex);
});
// Note: we can have as many dataPromise.done(...) as we want.
dataPromise.done(function(data){
alert("We asked it twice, we get it twice: " + data);
});
這裏咱們首先得到了dataPromise對象,而後調用.done方法來註冊一個當數據獲得解決咱們想要執行的函數。咱們也能夠調用.fail方法來處理錯誤結果。咱們還能夠執行更多咱們須要的.done或者.fail方法,由於jQuery相關的Promise實現會處理註冊和回調。
有了這種模式,如今想要實現更高級的異步代碼就更簡單了,並且jQuery已經提供了很是經常使用的$.when這個方法。
舉個例子,上面被嵌套的getData/getLocation 回調應該像這樣:
// assuming both getData and getLocation return their respective Promise
var combinedPromise = $.when(getData(), getLocation())
// function will be called when both getData and getLocation resolve
combinePromise.done(function(data,location){
alert("We got data: " + data + " and location: " + location);
});
使用jQuery.Deferred的美妙之處在於開發者能夠很輕鬆的實現異步函數,例如像這樣的getData:
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();
}
當getData()被調用時,它一開始會建立一個新的jQuery.Deferred對象(1),而後返回它的Promise對象(2),這樣getData就能夠註冊它的done和fail方法。接着,當XHR執行返回,它既可能解決這個deferred(3.1)也可能拒絕它(3.2)。當解決deferred時將會觸發全部done函數和其餘的promise函數(例如then和pipe),拒絕deferred則會執行fail函數。
使用場景:
數據訪問: 在遠程數據訪問時,異步遠程調用將會很明顯的影響用戶體驗。並且在本地數據中像低級的API(SQLite和IndexedDB)自己就是異步的。Deferred API的 $.when和 .pipe在同步和鏈式異步子查詢時很是強大。
UI動畫: 編寫一個或多個動畫的transitionEnd事件是一件很是乏味的事,特別是當動畫師CSS3動畫和JS的混合。將animarion函數用Deferred包裝起來能夠顯著地減小代碼複雜度和提高靈活性。甚至一個簡單通用的包裝器函數像cssAnimation(className)返回一個Promise對象(當transitionEnd時獲得解決)都將受益不淺。
UI組件顯示: 這個有點高級,可是高級的HTML組件框架應該也使用Deferred。當一個應用程序須要顯示不一樣部分的用戶界面,將組件經過Deferred封裝能夠更大的控制生命週期。
任何的異步API: 將瀏覽器API經過Deferred包裝,在字面上一個只用加4-5行代碼,但卻會極大地簡化應用程序代碼。
緩存化: 這是一種附帶的好處,但在一些場合裏這會很是有用。在異步調用時可將Deferred 對象緩存起來。好處是調用者不須要知道調用是否被解決或者正在被解決中,它的回調函數將會徹底以相同的方式被調用。
結論
$.Deferred概念很簡單,可是須要一段時間才能掌握好它。掌握js異步編程對任何HTML5應用程序開發者而言是很必要的,並且Promise模式使異步編程更可靠和強大!