這篇文章,我們一起探索一下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(
>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 );
複製程式碼
程式碼如下:
function getgets考慮$.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” );
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(>).then(function(){
console.log( after requests succ 」 );
}).fail(function(){ console.log( “something went wrong!” );
});
deferreds在ajax的幕後操作中使用並不意味著它們無法在別處使用。 在本節中,我們將看到在一些解決方案中,使用deferreds將有助於抽象化非同步的行為,並解耦我們的程式碼。
非同步快取 當涉及到非同步任務,快取可以是一個有點苛刻的,因為你必須確保對於同一個key任務僅執行一次。因此,程式碼需要以某種方式追蹤入站任務。 例如下面的程式碼片段:
複製程式碼
程式碼如下:
程式碼如下:
快取機制需要確保腳本不管是否已經存在於緩存,只能被要求一次。 因此,為了快取系統可以正確地處理請求,我們最終需要寫出一些邏輯來追蹤綁定到給定url上的回調。
值得慶幸的是,這恰好是deferred所實現的那種邏輯,因此我們可以這樣來做:
複製代碼
程式碼如下:
var cachedScriptPromises = {}; $.cachedGetScript = function( url, callback ) { $.cachedGetScript = function( url, callback ) { if. ) { 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>🎜} ;
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將建立一個新的快取庫,並傳回一個新的快取檢索函數。現在,我們擁有了一個通用的快取工廠,它很容易實現涉及從快取中取值的邏輯場景。
複製程式碼
程式碼如下:
$.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; >
程式碼如下:
無論image.png是否已經被加載,或者正在加載過程中,快取都會正常工作。
快取資料的API回應
哪些你的頁面的生命週期過程中被認為是不可變的API請求,也是快取完美的候選場景。 例如,執行以下操作:
複製程式碼 程式碼如下:
error: defer.reject
});
});
程式程式碼>允許你在Twitter上進行搜索,同時緩存它們:
複製代碼
代碼如下:
代碼如下: $.searchTwitter( "jQuery Deferred", callback1 ); $.searchTwitter( "jQuery Deferred", callback2 );
定時 基於deferred的快取並不限定於網路請求;它也可以被用於定時目的。
例如,您可能需要在網頁上給定一段時間後執行一個動作,來吸引用戶對某個不容易引起注意的特定功能的關注或處理一個延時問題。 雖然setTimeout適合大多數用例,但在計時器出發後甚至理論上過期後就無法提供解決方案。 我們可以使用以下的快取系統來處理:
var ready ;
$(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( s, arguments );
}
});
}).promise();
};
然後,我們可以用$.when()同步化不同的動畫:
複製程式碼
程式碼如下:
var fadeDiv1 =
var fadeDiv1 = kvar( #div1" ).animatePromise({ opacity: 0 }),
fadeDiv2In = $( "#div1" ).animatePromise({ opacity: 1 }, "fast" );
$.when( fadeDiv1Out, fadeDiv2In ).done(function() {
/* both animations ended */
});
複製程式碼
程式碼如下:
$. 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 */
});
一次性事件
複製程式碼
程式碼如下:
var buttonClicked = false;
$( "#myButton" ).click(function() {
if ( !buttonClicked ) {
buttonClicked = true;
mm;
showPanel();
}
});
複製程式碼 程式碼如下:
if ( buttonClicked ) { /* perform specific action */ }
這是一個非常耦合的解決方法。 如果你想添加一些其他的操作,你必須編輯綁定程式碼或拷貝一份。 如果你不這樣做,你唯一的選擇是測試buttonClicked。由於buttonClicked可能是false,新的程式碼可能永遠不會被執行,因此你 可能會失去這個新的動作。
使用deferreds我們可以做的更好(為簡化起見,下面的程式碼將只適用於一個單一的元素和一個單一的事件類型,但它可以很容易地擴展為多個事件類型的集合):
fn. 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 );
}
el.bind( , deferCallback )
element.data( "bind_once_defer_" event , defer );
}
return defer.done( callback ).promise();
}
>程式碼的工作原理如下:
•檢查該元素是否已經綁定了一個給定事件的deferred物件
•如果沒有,創建它,使它在觸發該事件的第一時間解決
•然後在deferred上綁定給定的回調並回傳promise
程式碼雖然很冗長,但它會簡化相關問題的處理。 讓我們先定義一個輔助方法:
程式碼如下:
程式碼如下:
return this.bindOnce( "click", callback );
複製程式碼
程式碼如下:
var openPanel = $( myButton" ).firstClick();
openPanel.done( initializeData );
openPanel.done( showPanel ); 如果我們需要執行一些動作,只有當面板打開以後,所有我們需要的是這樣的:
複製程式碼
程式碼如下:
程式碼如下:
Panle .done(function() { /* perform specific action */ });
如果面板沒有打開,行動將得到延遲到單擊該按鈕時。
假如,我們有一個按鈕,可以打開一個面板,請求其內容然後淡入內容。使用我們前面定義的助手方法,我們可以這樣做:
複製程式碼
程式碼如下:
程式碼如下:
程式碼如下:
程式碼如下: var panel = $( "#myPanel" ); panel.firstClick(function() {
$.when(
$.get( "panel.html" ),
panel.slideDownPromise ()
).done(function( ajaxResponse ) {
panel.html( ajaxResponse[ 0 ] ).fadeIn();
});
});
});
});
}); 🎜>
在第一次點擊時載入圖像並打開面板 假如,我們已經的面板有內容,但我們只希望當第一次單擊按鈕時載入圖像並且當所有圖像加載成功後淡入圖像。 HTML程式碼如下:
複製程式碼 程式碼如下:
我們使用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的格式如下:
意思非常簡單:
•image1.png,第三個影像立即顯示,一秒鐘後第一個影像顯示
•image2.png 一秒後顯示第二張影像,兩秒鐘後顯示第四個影像
我們將如何實現?
$( "img" ). function() {
var element = $( this ),
src = element.attr( "data-src" ),
after = element.attr( "data-after" );
if ( src ) {
$.when(
$.loadImage( src ),
$.afterDOMReady( after )
).then(function() {
element. 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實作 ,會非常容易的從你的程式碼中分離出非同步任務。 這樣的話,你可以很容易的從你的應用程式中分離邏輯。