使用 jQuery Deferred 和 Promise 建立響應式應用程序
抽象來講,deferreds 能夠理解爲表示須要長時間才能完成的耗時操做的一種方式,相比於阻塞式函數它們是異步的,而不是阻塞應用程序等待其完成而後返回結果。deferred對 象會當即返回,而後你能夠把回調函數綁定到deferred對象上,它們會在異步處理完成後被調用。javascript
Promise
你可能已經閱讀過一些關於promise和deferreds實現細節的資料。在本章節中,咱們大體介紹下promise如何工做,這些在幾乎全部的支持deferreds的javascript框架中都是適用的。
通常狀況下,promise做爲一個模型,提供了一個在軟件工程中描述延時(或未來)概念的解決方案。它背後的思想咱們已經介紹過:不是執行一個方法而後阻塞應用程序等待結果返回,而是返回一個promise對象來知足將來值。
舉一個例子會有助於理解,假設你正在建設一個web應用程序, 它很大程度上依賴第三方api的數據。那麼就會面臨一個共同的問題:咱們沒法獲悉一個API響應的延遲時間,應用程序的其餘部分可能會被阻塞,直到它返回 結果。Deferreds 對這個問題提供了一個更好的解決方案,它是非阻塞的,而且與代碼徹底解耦 。
Promise/A提議’定義了一個’then‘方法來註冊回調,當處理函數返回結果時回調會執行。它返回一個promise的僞代碼看起來是這樣的:
php
promise = callToAPI( arg1, arg2, ...); promise.then(function( futureValue ) { /* handle futureValue */ }); promise.then(function( futureValue ) { /* do something else */ });
此外,promise回調會在處於如下兩種不一樣的狀態下執行:
resolved:在這種狀況下,數據是可用
rejected:在這種狀況下,出現了錯誤,沒有可用的值
幸運的是,'then'方法接受兩個參數:一個用於promise獲得瞭解決(resolved),另外一個用於promise拒絕(rejected)。讓咱們回到僞代碼:
html
promise.then( function( futureValue ) { /* we got a value */ } , function() { /* something went wrong */ } );
在某些狀況下,咱們須要得到多個返回結果後,再繼續執行應用程序(例如,在用戶能夠選擇他們感興趣的選項前,顯示一組動態的選項)。這種狀況下,'when'方法能夠用來解決全部的promise都知足後才能繼續執行的場景。
java
when( promise1, promise2, ... ).then(function( futureValue1, futureValue2, ... ) { /* all promises have completed and are resolved */ });
一個很好的例子是這樣一個場景,你可能同時有多個正在運行的動畫。 若是不跟蹤每一個動畫執行完成後的回調,很難作到在動畫完成後執行下一步任務。然而使用promise和‘when’方式卻能夠很直截了當的表示: 一旦動畫執行完成,就能夠執行下一步任務。最終的結果是咱們能夠能夠簡單的用一個回調來解決多個動畫執行結果的等待問題。 例如:
react
when( function(){ /* animation 1 */ /* return promise 1 */ }, function(){ /* animation 2 */ /* return promise 2 */ } ).then(function(){ /* once both animations have completed we can then run our additional logic */ });
這意味着,基本上能夠用非阻塞的邏輯方式編寫代碼並異步執行。 而不是直接將回調傳遞給函數,這可能會致使緊耦合的接口,經過promise模式能夠很容易區分同步和異步的概念。
在下一節中,咱們將着眼於jQuery實現的deferreds,你可能會發現它明顯比如今所看到的promise模式要簡單。web
jQuery的Deferreds
jQuery在1.5版本中首次引入了deferreds。它 所實現的方法與咱們以前描述的抽象的概念沒有大的差異。原則上,你得到了在將來某個時候獲得‘延時’返回值的能力。在此以前是沒法單獨使用的。 Deferreds 做爲對ajax模塊較大重寫的一部分添加進來,它遵循了CommonJS的promise/ A設計。1.5和先前的版本包含deferred功能,可使$.ajax() 接收調用完成及請求出錯的回調,但卻存在嚴重的耦合。開發人員一般會使用其餘庫或工具包來處理延遲任務。新版本的jQuery提供了一些加強的方式來管理 回調,提供更加靈活的方式創建回調,而不用關心原始的回調是否已經觸發。 同時值得注意的是,jQuery的遞延對象支持多個回調綁定多個任務,任務自己能夠既能夠是同步也能夠是異步的。
您能夠瀏覽下表中的遞延功能,有助於瞭解哪些功能是你須要的:
jQuery.Deferred() 建立一個新的Deferred對象的構造函數,能夠帶一個可選的函數參數,它會在構造完成後被調用。
jQuery.when() 經過該方式來執行基於一個或多個表示異步任務的對象上的回調函數
jQuery.ajax() 執行異步Ajax請求,返回實現了promise接口的jqXHR對象
deferred.then(resolveCallback,rejectCallback) 添加處理程序被調用時,遞延對象獲得解決或者拒絕的回調。
deferred.done()
當延遲成功時調用一個函數或者數組函數.
deferred.fail()
當延遲失敗時調用一個函數或者數組函數.。
deferred.resolve(ARG1,ARG2,...) 調用Deferred對象註冊的‘done’回調函數並傳遞參數
deferred.resolveWith(context,args) 調用Deferred對象註冊的‘done’回調函數並傳遞參數和設置回調上下文
deferred.isResolved 肯定一個Deferred對象是否已經解決。
deferred.reject(arg1,arg2,...) 調用Deferred對象註冊的‘fail’回調函數並傳遞參數
deferred.rejectWith(context,args) 調用Deferred對象註冊的‘fail’回調函數並傳遞參數和設置回調上下文
deferred.promise() 返回promise對象,這是一個僞造的deferred對象:它基於deferred而且不能改變狀態因此能夠被安全的傳遞
jQuery延時實現的核心是jQuery.Deferred:一個能夠鏈式調用的構造函數。...... 須要注意的是任何deferred對象的默認狀態是unresolved, 回調會經過 .then() 或 .fail()方法添加到隊列,並在稍後的過程當中被執行。
下面這個$.when() 接受多個參數的例子
ajax
function successFunc(){ console.log( 「success!」 ); } function failureFunc(){ console.log( 「failure!」 ); } $.when( $.ajax( "/main.php" ), $.ajax( "/modules.php" ), $.ajax( 「/lists.php」 ) ).then( successFunc, failureFunc );
在$.when() 的實現中有趣的是,它並不是僅能解析deferred對象,還能夠傳遞不是deferred對象的參數,在處理的時候會把它們當作deferred對象並立 即執行回調(doneCallbacks)。 這也是jQuery的Deferred實現中值得一提的地方,此外,deferred.then()還爲deferred.done和 deferred.fail()方法在deferred的隊列中增長回調提供支持。
利用前面介紹的表中提到的deferred功能,咱們來看一個代碼示例。 在這裏,咱們建立一個很是基本的應用程序:經過$.get方法(返回一個promise)獲取一條外部新聞源(1)而且(2)獲取最新的一條回覆。 同時程序還經過函數(prepareInterface())實現新聞和回覆內容顯示容器的動畫。
爲了確保在執行其餘相關行爲前,上面的這三個步驟確保完成,咱們使用$.when()。根據您的須要 .then()和.fail() 處理函數能夠被用來執行其餘程序邏輯。
json
function getLatestNews() { return $.get( 「latestNews.php」, function(data){ console.log( 「news data received」 ); $( 「.news」 ).html(data); } ); } function getLatestReactions() { return $.get( 「latestReactions.php」, function(data){ console.log( 「reactions data received」 ); $( 「.reactions」 ).html(data); } ); } function prepareInterface() { return $.Deferred(function( dfd ) { var latest = $( 「.news, .reactions」 ); latest.slideDown( 500, dfd.resolve ); latest.addClass( 「active」 ); }).promise(); } $.when( getLatestNews(), getLatestReactions(), prepareInterface() ).then(function(){ console.log( 「fire after requests succeed」 ); }).fail(function(){ console.log( 「something went wrong!」 ); }); deferreds在ajax的幕後操做中使用並不意味着它們沒法在別處使用。 在本節中,咱們將看到在一些解決方案中,使用deferreds將有助於抽象掉異步的行爲,並解耦咱們的代碼。 異步緩存 當涉及到異步任務,緩存能夠是一個有點苛刻的,由於你必須確保對於同一個key任務僅執行一次。所以,代碼須要以某種方式跟蹤入站任務。 例以下面的代碼片斷: $.cachedGetScript( url, callback1 ); $.cachedGetScript( url, callback2 );
緩存機制須要確保 腳本無論是否已經存在於緩存,只能被請求一次。 所以,爲了緩存系統能夠正確地處理請求,咱們最終須要寫出一些邏輯來跟蹤綁定到給定url上的回調。
值得慶幸的是,這剛好是deferred所實現的那種邏輯,所以咱們能夠這樣來作:
api
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( requestFunction ) { var cache = {}; return function( key, callback ) { if ( !cache[ key ] ) { cache[ key ] = $.Deferred(function( defer ) { requestFunction( defer, key ); }).promise(); } return cache[ key ].done( callback ); }; }
如今具體的請求邏輯已經抽象出來,咱們能夠從新寫cachedGetScript:
$.cachedGetScript = $.createCache(function( defer, url ) {
$.getScript( url ).then( defer.resolve, defer.reject );
});
每次調用createCache將建立一個新的緩存庫,並返回一個新的高速緩存檢索函數。如今,咱們擁有了一個通用的緩存工廠,它很容易實現涉及從緩存中取值的邏輯場景。
圖片加載
另外一個候選場景是圖像加載:確保咱們不加載同一個圖像兩次,咱們可能須要加載圖像。 使用createCache很容易實現:
$.loadImage = $.createCache(function( defer, url ) { var image = new Image(); function cleanUp() { image.onload = image.onerror = null; } defer.then( cleanUp, cleanUp ); image.onload = function() { defer.resolve( url ); }; image.onerror = defer.reject; image.src = url; });
接下來的代碼片斷以下:
$.loadImage( "my-image.png" ).done( callback1 );
$.loadImage( "my-image.png" ).done( callback2 );
不管image.png是否已經被加載,或者正在加載過程當中,緩存都會正常工做。
緩存數據的API響應
哪些你的頁面的生命週期過程當中被認爲是不可變的API請求,也是緩存完美的候選場景。 好比,執行如下操做:
$.searchTwitter = $.createCache(function( defer, query ) { $.ajax({ url: "http://search.twitter.com/search.json", data: { q: query }, dataType: "jsonp", success: defer.resolve, error: defer.reject }); });
程序容許你在Twitter上進行搜索,同時緩存它們:
$.searchTwitter( "jQuery Deferred", callback1 );
$.searchTwitter( "jQuery Deferred", callback2 );
定時
基於deferred的緩存並不限定於網絡請求;它也能夠被用於定時目的。
例如,您可能須要在網頁上給定一段時間後執行一個動做,來吸引用戶對某個不容易引發注意的特定功能的關注或處理一個延時問題。 雖然setTimeout適合大多數用例,但在計時器出發後甚至理論上過時後就沒法提供解決辦法。 咱們可使用如下的緩存系統來處理:
var readyTime; $(function() { readyTime = jQuery.now(); }); $.afterDOMReady = $.createCache(function( defer, delay ) { delay = delay || 0; $(function() { var delta = $.now() - readyTime; if ( delta >= delay ) { defer.resolve(); } else { setTimeout( defer.resolve, delay - delta ); } }); });
新的afterDOMReady輔助方法用最少的計數器提供了domReady後的適當時機。 若是延遲已通過期,回調會被立刻執行。
同步多個動畫
動畫是另外一個常見的異步任務範例。 然而在幾個不相關的動畫完成後執行代碼仍然有點挑戰性。儘管在jQuery1.6中才提供了在動畫元素上取得promise對象的功能,但它是很容易的手動實現:
$.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(); };
而後,咱們可使用$.when()同步化不一樣的動畫:
var fadeDiv1Out = $( "#div1" ).animatePromise({ opacity: 0 }), fadeDiv2In = $( "#div1" ).animatePromise({ opacity: 1 }, "fast" ); $.when( fadeDiv1Out, fadeDiv2In ).done(function() { /* both animations ended */ });
咱們也可使用一樣的技巧,創建了一些輔助方法:
$.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(); }; });
而後想下面這樣使用新的助手代碼來同步動畫:
$.when( $( "#div1" ).fadeOutPromise(), $( "#div2" ).fadeInPromise( "fast" ) ).done(function() { /* both animations are done */ });
一次性事件
雖然jQuery提供你可能須要的全部的時間綁定方法,但當事件僅須要處理一次時,狀況可能會變得有點棘手。( 與$.one() 不一樣 )
例如,您可能但願有一個按鈕,當它第一次被點擊時打開一個面板,面板打開以後,執行特定的初始化邏輯。 在處理這種狀況時,人們一般會這樣寫代碼:
var buttonClicked = false; $( "#myButton" ).click(function() { if ( !buttonClicked ) { buttonClicked = true; initializeData(); showPanel(); } });
不久後,你可能會在面板打開以後點擊按鈕時添加一些操做,以下:
if ( buttonClicked ) { /* perform specific action */ }
這是一個很是耦合的解決辦法。 若是你想添加一些其餘的操做,你必須編輯綁定代碼或拷貝一份。 若是你不這樣作,你惟一的選擇是測試buttonClicked。因爲buttonClicked多是false,新的代碼可能永遠不會被執行,所以你 可能會失去這個新的動做。
使用deferreds咱們能夠作的更好 (爲簡化起見,下面的代碼將只適用於一個單一的元素和一個單一的事件類型,但它能夠很容易地擴展爲多個事件類型的集合):
$.fn.bindOnce = function( event, callback ) { var element = $( this[ 0 ] ), defer = element.data( "bind_once_defer_" + event ); if ( !defer ) { defer = $.Deferred(); function deferCallback() { element.unbind( event, deferCallback ); defer.resolveWith( this, arguments ); } element.bind( event, deferCallback ) element.data( "bind_once_defer_" + event , defer ); } return defer.done( callback ).promise(); };
該代碼的工做原理以下:
檢查該元素是否已經綁定了一個給定事件的deferred對象
若是沒有,建立它,使它在觸發該事件的第一時間解決
而後在deferred上綁定給定的回調並返回promise
代碼雖然很冗長,但它會簡化相關問題的處理。 讓咱們先定義一個輔助方法:
$.fn.firstClick = function( callback ) { return this.bindOnce( "click", callback ); };
而後,以前的邏輯能夠重構以下:
var openPanel = $( "#myButton" ).firstClick(); openPanel.done( initializeData ); openPanel.done( showPanel );
若是咱們須要執行一些動做,只有當面板打開之後,全部咱們須要的是這樣的:
openPanel.done(function() { /* perform specific action */ });
若是面板沒有打開,行動將獲得延遲到單擊該按鈕時。
組合助手
單獨看以上每一個例子,promise的做用是有限的 。 然而,promise真正的力量是把它們混合在一塊兒。
在第一次點擊時加載面板內容並打開面板
假如,咱們有一個按鈕,能夠打開一個面板,請求其內容而後淡入內容。使用咱們前面定義的助手方法,咱們能夠這樣作:
var panel = $( "#myPanel" ); panel.firstClick(function() { $.when( $.get( "panel.html" ), panel.slideDownPromise() ).done(function( ajaxResponse ) { panel.html( ajaxResponse[ 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屬性描述圖片的真實路徑。 那麼使用promise助手來解決該用例的代碼以下: $( "#myButton" ).firstClick(function() { var panel = $( "#myPanel" ), promises = []; $( "img", panel ).each(function() { var image = $( this ), src = element.attr( "data-src" ); if ( src ) { promises.push( $.loadImage( src ).then( function() { image.attr( "src", src ); }, function() { image.attr( "src", "error.png" ); } ) ); } }); promises.push( panel.slideDownPromise() ); $.when.apply( null, promises ).done(function() { panel.fadeIn(); }); });
這裏的竅門是跟蹤全部的LoadImage 的promise,接下來加入面板slideDown動畫。 所以首次點擊按鈕時,面板將slideDown而且圖像將開始加載。 一旦完成向下滑動面板和已加載的全部圖像,面板纔會淡入。
在特定延時後加載頁面上的圖像
假如,咱們要在整個頁面實現遞延圖像顯示。 要作到這一點,咱們須要的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.attr( "data-src" ), after = element.attr( "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.attr( "data-src" ), after = element.attr( "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(); }); } ); } });
這裏,咱們首先在嘗試加載圖片以前等待延遲條件知足。當你想在頁面加載時限制網絡請求的數量會很是有意義。
結論 正如你看到的,即便在沒有Ajax請求的狀況下,promise也很是有用的。經過使用jQuery 1.5中的deferred實現 ,會很是容易的從你的代碼中分離出異步任務。 這樣的話,你能夠很容易的從你的應用程序中分離邏輯。