jquery源碼分析(五)——Deferred 延遲對象

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的實現

  1. 接受若干個對象,參數僅一個且非Deferred對象將當即執行回調函數
  2. Deferred對象和非Deferred對象混雜時,對於非Deferred對象remaining減1
  3. Deferred對象總數 = 內部構建的Deferred對象 + 所傳參數中包含的Deferred對象
  4. 所傳參數中全部Deferred對象每當resolve時remaining減1,直到爲0時(全部都resolve)執行回調

 

(五)、你們可能對ajax異步回調比較熟悉,那下面用ajax分析如何使用deferred對象。

(1) done/resolve

function cb() {

   alert( 'success' )
}
var  deferred = $.Deferred()
deferred.done(cb)
setTimeout( function () {
     deferred.resolve()
}, 3000)
這裏使用setTimeout來模擬ajax請求(下同),在HTTP中表示後臺返回成功狀態(如200)時使用,執行deferred.resolve(),則會觸發請求成功後的回調函數cb。
 

(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)

相關文章
相關標籤/搜索