jQuery Deferred和Promise建立響應式應用程式詳細介紹

來源:互聯網
上載者:User

這篇文章,我們一起探索一下 JavaScript 中的 Deferred 和 Promise 的概念,它們是 JavaScript 工具包(如Dojo和MochiKit)中非常重要的一個功能,最近也首次亮相於 流行的 JavaScript 庫 jQuery(已經是1.5版本的事情了)。 Deferred 提供了一個抽象的非阻塞的解決方案(如 Ajax 請求的響應),它建立一個 “promise” 對象,其目的是在未來某個時間點返回一個響應。如果您之前沒有接觸過 “promise”,我們將會在下面做詳細介紹。

抽象來說,deferreds 可以理解為表示需要長時間才能完成的耗時操作的一種方式,相比於阻塞式函數它們是非同步,而不是阻塞應用程式等待其完成然後返回結果。deferred對 象會立即返回,然後你可以把回呼函數綁定到deferred對象上,它們會在非同步處理完成後被調用。

Promise
  你可能已經閱讀過一些關於promise和deferreds實現細節的資料。在本章節中,我們大致介紹下promise如何工作,這些在幾乎所有的支援deferreds的javascript架構中都是適用的。

  一般情況下,promise作為一個模型,提供了一個在軟體工程中描述延時(或將來)概念的解決方案。它背後的思想我們已經介紹過:不是執行一個方法然後阻塞應用程式等待結果返回,而是返回一個promise對象來滿足未來值。

  舉一個例子會有助於理解,假設你正在建設一個web應用程式, 它很大程度上依賴第三方api的資料。那麼就會面臨一個共同的問題:我們無法獲悉一個API響應的延遲時間,應用程式的其他部分可能會被阻塞,直到它返回 結果。Deferreds 對這個問題提供了一個更好的解決方案,它是非阻塞的,並且與代碼完全解耦 。

  Promise/A提議'定義了一個'then‘方法來註冊回調,當處理函數返回結果時回調會執行。它返回一個promise的虛擬碼看起來是這樣的:
複製代碼 代碼如下:
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)。讓我們回到虛擬碼:

複製代碼 代碼如下:
promise.then( function( futureValue ) {
/* we got a value */
} , function() {
/* something went wrong */
} );


在某些情況下,我們需要獲得多個返回結果後,再繼續執行應用程式(例如,在使用者可以選擇他們感興趣的選項前,顯示一組動態選項)。這種情況下,'when'方法可以用來解決所有的promise都滿足後才能繼續執行的情境。

複製代碼 代碼如下:
when(
promise1,
promise2,
...
).then(function( futureValue1, futureValue2, ... ) {
/* all promises have completed and are resolved */
});


一個很好的例子是這樣一個情境,你可能同時有多個正在啟動並執行動畫。 如果不跟蹤每個動畫執行完成後的回調,很難做到在動畫完成後執行下一步任務。然而使用promise和‘when'方式卻可以很直截了當的表示: 一旦動畫執行完成,就可以執行下一步任務。最終的結果是我們可以可以簡單的用一個回調來解決多個動畫執行結果的等待問題。 例如:

複製代碼 代碼如下:
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模式要簡單。

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() 接受多個參數的例子
複製代碼 代碼如下:
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() 處理函數可以被用來執行其他程式邏輯。

複製代碼 代碼如下:
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所實現的那種邏輯,因此我們可以這樣來做:

複製代碼 代碼如下:
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提供你可能需要的所有的時間Binder 方法,但當事件僅需要處理一次時,情況可能會變得有點棘手。( 與$.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實現 ,會非常容易的從你的代碼中分離出非同步任務。 這樣的話,你可以很容易的從你的應用程式中分離邏輯。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.