javascript的異步編程javascript
爲何要使用異步編程? java
JS是單線程語言,就簡單性而言,把每一件事情(包括GUI事件和渲染)都放在一個線程裏來處理是一個很好的程序模型,由於這樣就無需再考慮線程同步這些複雜問題。然而從另外一方面來看,它也暴露了實際開發中的一個嚴重問題,單線程環境看起來對用戶請求響應迅速,可是當線程忙於處理其它事情時,就不能對用戶的鼠標點擊和鍵盤操做作出響應。所以提出了異步需求,以此來解放當前線程,能夠處理其餘業務,待回調請求數據返回再發起回來處理。web
何謂異步?關於這個問題,司徒正美給出如下解釋:ajax
籠統地說,異步在javascript就是延時執行。嚴格來講,javascript中的異步編程能力都是由BOM與DOM提供的,如setTimeout,XMLHttpRequest,還有DOM的事件機制,還有HTML5新增長的webwork, postMessage,等等不少。這些東西都有一個共同的特色,就是擁有一個回調函數,實現控制反轉。因爲控制反轉是更深奧的問題,這裏不想展開。不過有點能夠確認的,回調函數的存在打斷了原來的執行流程,讓它們自行在適當的時機出現並執行,這是個很是便捷的模式。對比主動式的輪詢,你就知它多麼節能。編程
相比在同步編程,代碼基本上自上向下執行,在異步編程,處理異步請求或訂閱返回結果的代碼就要寫到回調函數中,因爲代碼之間存在依賴,回調函數套回調函數的狀況也司空見慣了,這種套嵌結構對之後的維護來講簡直是地獄噩夢。由此引出另一個咱們不得不面對的問題,try...catch沒法捕捉幾毫秒以後發生的異常。另外,除了setTimeout外,異步編程基本上由事件機制承擔的,它們的回調函數何時發生基本上都是未知數,可能因爲後臺發生系統級錯誤,沒法再發出響應,或者,系統忙碌,一時半刻響應不過來,這兩種狀況咱們也必需提供一個策略,中斷這操做,也就是所謂的abort,這些都是異步編程的所要處理的課題數組
Deferred對象是用來解決JS中的異步編程,它遵循 Common Promise/A 規範,在jQuery代碼自身代碼中有四處使用地方,分別是promise方法、DOM ready、ajax模塊、動畫模塊。既然談到 Common Promise/A 規範就先來了解下這個規範說了什麼。promise
Promise(中文翻譯:承諾)其實爲一個有限狀態機,共有三種狀態:pending(執行中)、resolve(執行成功)和rejected(執行失敗)。閉包
其中pending爲初始狀態,resolve和rejected爲結束狀態(結束狀態表示promise的生命週期已結束)。狀態轉換關係爲:app
pending->resolve,pending->rejected。異步
隨着狀態的轉換將觸發各類事件(如執行成功事件、執行失敗事件等)。promise以未完成的狀態開始,若是成功它將會是完成態,若是失敗將會是失敗態。當一個promise移動到完成態,全部註冊到它的成功回調將被調用,並且會將成功的結果值傳給它。另外,任何註冊到promise的成功回調,將會在狀態機切換到完成狀態之後當即被調用。
來看下代碼的實現:
Deferred: function( func ) { var tuples = [//依賴了Calback回調隊列管理。 // action, add listener, listener list, final state [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], [ "notify", "progress", jQuery.Callbacks("memory") ] ],//tuples 建立三個$.Callbacks對象,分別表示成功,失敗,處理中三種狀態
state = "pending", promise = { state: function() {//state的get訪問器:經過閉包訪問狀態機的狀態,確保只能訪問到Deferred對象的state,而沒法修改state return state; }, always: function() {//不管最終狀態爲成功抑或是失敗都會觸發always,根據state執行deferred對象的成功回調或失敗回調 deferred.done( arguments ).fail( arguments ); return this; }, then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; return jQuery.Deferred(function( newDefer ) { jQuery.each( tuples, function( i, tuple ) {//逐必定義deferred對象成功、失敗、執行中的處理函數執行以後的處理函數。 var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; // deferred[ done | fail | progress ] for forwarding actions to newDefer deferred[ tuple[1] ](function() { var returned = fn && fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { returned.promise() .done( newDefer.resolve ) .fail( newDefer.reject ) .progress( newDefer.notify ); } else { newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); } }); }); fns = null; }).promise(); }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) { return obj != null ? jQuery.extend( obj, promise ) : promise; } }, deferred = {}; // Keep pipe for back-compat promise.pipe = promise.then; // Add list-specific methods jQuery.each( tuples, function( i, tuple ) {//回調函數的存入 var list = tuple[ 2 ], stateString = tuple[ 3 ]; // promise[ done | fail | progress ] = list.add promise[ tuple[1] ] = list.add;//設計精妙,在狀態機上掛載 done | fail | progress 方法,實際應用對應爲Callback對象的add方法 // Handle state if ( stateString ) {//若是存在最終狀態:成功或失敗都存在最終狀態,處理中不存在undefined list.add(function() { // state = [ resolved | rejected ] state = stateString; // [ reject_list | resolve_list ].disable; progress_list.lock }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );//給當前回調隊列Callback增長三個回調函數,i^1爲異或 成功則修改失敗回調隊列disable(不執行),失敗則修改爲功回調隊列disable不執行 } // deferred[ resolve | reject | notify ] deferred[ tuple[0] ] = function() {//定義觸發函數 deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); return this; }; deferred[ tuple[0] + "With" ] = list.fireWith; }); // Make the deferred a promise promise.promise( deferred );//擴展deferred,繼承promise屬性——即賦予deferred對象狀態機屬性 // Call given func if any if ( func ) { func.call( deferred, deferred ); } // All done! return deferred; }, // Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, resolveValues = slice.call( arguments ), length = resolveValues.length, // the count of uncompleted subordinates remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, // the master Deferred. If resolveValues consist of only a single Deferred, just use that. deferred = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values updateFunc = function( i, contexts, values ) { return function( value ) { contexts[ i ] = this; values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; if ( values === progressValues ) { deferred.notifyWith( contexts, values ); } else if ( !(--remaining) ) { deferred.resolveWith( contexts, values ); } }; }, progressValues, progressContexts, resolveContexts; // add listeners to Deferred subordinates; treat others as resolved if ( length > 1 ) { progressValues = new Array( length ); progressContexts = new Array( length ); resolveContexts = new Array( length ); for ( ; i < length; i++ ) { if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { resolveValues[ i ].promise() .done( updateFunc( i, resolveContexts, resolveValues ) ) .fail( deferred.reject ) .progress( updateFunc( i, progressContexts, progressValues ) ); } else { --remaining; } } } // if we're not waiting on anything, resolve the master if ( !remaining ) { deferred.resolveWith( resolveContexts, resolveValues ); } return deferred.promise(); }
(一)、首先來理解下deferred功能:
一、異步隊列是一個鏈式對象,加強對回調函數Callback的管理和調用,用於處理異步任務。
二、異步隊列有三種狀態:處理中(progress),成功(resolved),失敗(rejected)。
三、執行哪些回調函數依賴於狀態,狀態變爲成功(resolved)或失敗(rejected)後,將保持不變。
四、回調函數的綁定能夠是同步,也能夠是異步的,便可以在任什麼時候候綁定
(二)、$.Deferred的實現
一、首先代碼中定義tuples數組:這個數組是幹嗎的呢?
(1)、tuples[i][0]:定義deferred對象的觸發回調函數列表執行(函數名):deferred.resolve,deferred.reject,deferred.notify
(2)、tuples[i][1]:同上,定義添加回調函數的函數名:done、fail、progress
(3)、tuples[i][2]:這裏依賴$.Callbacks對象,用它建立了三個$.Callbacks對象,分別管理成功,失敗,處理中回調函數隊列
(4)、tuples[i][3]:deferred最終狀態(第三組數據除外): 定義了成功最終狀態(resolved),失敗的最終狀態(rejected)
二、建立了一個狀態機promise對象,具備state、always、then、primise方法
(1)state:獲取閉包屬性state。默認pending;
(2)always:設置deferred對象成功和失敗處理隊列,根據state執行deferred對象的成功回調或失敗回調
(3)then:
(4)promise:將賦予對象狀態機特性(內部同過$.extend來擴展),即新對象將擁有state、always、then、primise方法,即狀態機的屬性
三、jQuery.each遍歷tuples:設置promise對象done,fail,progress屬性引用Callback對象add方法,同時給deferred對象擴充6個方法
(1)resolve/reject/notify 相似 Callbacks.fire,調用對應**With,執行回調函數
(2)resolveWith/rejectWith/notifyWith 是 Callbacks.fireWith 隊列方法引用
四、經過擴展primise對象生成最終的Deferred對象,返回該對象。以下圖:
從上圖能夠清晰看出:deferred對象的state、always 、promise、then是從promise對象擴展而來的,其餘方法pipe <==>then ,
done、fail、progress是對象三個Callback對象的回調add方法;——回調方法/事件訂閱
reject、resolve、notify爲觸發相應執行回調隊列的fire方法——通知方法/事件發佈
rejectWith、resolveWith、notifyWith爲執行相應回調隊列的fireWidth方法——通知方法/事件發佈
(三)、具體來分析deferred對象上的方法:
(1)、deferred.done()—— 訂閱狀態爲成功(resolved)時回調函數,保存到對應的Callback對象
(2)、deferred.fail()—— 訂閱狀態爲失敗(rejected)時回調函數,保存到對應Callback對象
(3)、deferred.progress()——訂閱狀態爲處理中(pending)時回調函數,保存到對應Callback對象
(4)、deferred.then()——增長成功回調函數和失敗回調函數到各自的隊列中;兩個參數能夠是數組或null,狀態爲成功(resolved)時當即調用成功回調函數,狀態爲失敗(rejected)時當即調用失敗回調函數
(5)、deferred.always()——增長回調函數,同時增長到成功隊列和失敗隊列,狀態已肯定(不管成功或失敗)時當即調用回調函數
(6)、deferred.resolve()——經過調用deferred.resolveWith()實現成功回調函數隊列調用
(7)、deferred.resolveWith()——使用指定的上下文和參數執行成功回調函數
(8)、deferred.reject()——經過調用deferred.rejectWith()實現失敗回調函數隊列調用
(9)、deferred.rejectWith()——使用指定的上下文和參數執行成功回調函數
(10)、deferred.pipe()——每次調用回調函數以前先調用傳入的成功過濾函數或失敗過濾函數,並將過濾函數的返回值做爲回調函數的參數最終返回一個只讀視圖(調用promise實現)
(11)、deferred.promise()——返回deferred的只讀視圖
(四)、$.when的實現
(五)、你們可能對ajax異步回調比較熟悉,那下面用ajax分析如何使用deferred對象。
(1) done/resolve
function
cb() {
alert(
'success'
)
}
var
deferred = $.Deferred()
deferred.done(cb)
setTimeout(
function
() {
deferred.resolve()
}, 3000)
(2)、fail/reject
function
cb() {
alert(
'fail'
)
}
var
deferred = $.Deferred()
deferred.fail(cb)
setTimeout(
function
() {
deferred.reject()
}, 3000)
在HTTP中表示後臺返回非成功狀態時使用,執行deferred.resolve() ,觸發請求失敗後的回調函數alert(fail)。
(3)、progress/notify
function
cb() {
alert(
'progress'
)
}
var
deferred = $.Deferred()
deferred.progress(cb)
setInterval(
function
() {
deferred.notify()
}, 2000)
注意到沒有,這裏使用在setInterval來模擬,HTTP中表示請求過程當中使用,請求過程當中不斷執行deferred.notify(),即
不斷執行回調函數。這可用在文件上傳時的loading百分比或進度條。
(4)、便利函數then,一次添加成功,失敗,進度回調函數
function fn1() {
alert('success')
}
function fn2() {
alert('fail')
}
function fn3() {
alert('progress')
}
var deferred = $.Deferred()
deferred.then(fn1, fn2, fn3)
調用then後還能夠繼續鏈式調用then添加多個不一樣回調函數,這個then也正是jQuery對 Common Promise/A 的實現。
(5)、使用always方法爲成功,失敗狀態添加同一個回調函數
var deferred = $.Deferred()
deferred.always(function() {
var state = deferred.state()
if ( state === 'resolved') {
alert('success')
} else if (state === 'rejected') {
alert('fail')
}
})
setTimeout(function() {
deferred.resolve()
//deferred.reject()
}, 3000)
回調函數中可使用deferred.state方法獲取異步過程當中的最終狀態,這裏我調用的是deferred.resolve,所以最後的狀態是resolved,表示成功。
(6)、when方法保證多個異步操做所有成功後纔回調
function fn1() {
alert('done1')
}
function fn2() {
alert('done2')
}
function fn3() {
alert('all done')
}
var deferred1 = $.Deferred()
var deferred2 = $.Deferred()
deferred1.done(fn1)
deferred2.done(fn2)
$.when(deferred1, deferred2).done(fn3)
setTimeout(function() {
deferred1.resolve()
deferred2.resolve()
}, 3000)
前後彈出了done一、done二、all done。 若是setTimeout中有一個reject了,fn3將不會被執行。
(7)、若是多個請求完成後纔算成功,1.5以前的是沒法解決的,如今則能夠用$.when搞定
var ajax1 = $.ajax(url1)var ajax2 = $.ajax(url2)$.when(ajax1, ajax2).done(success)