jQuery源碼閱讀(十三)---jQuery異步隊列模塊

上一篇博客分析了Callbacks實現原理,而在jQuery中,Ready函數的實現,異步隊列以及隊列模塊都用到了Callbacks對象。jQuery.ready函數在前面已經做了整理,所以這篇博客主要是分析Deffered(異步隊列)和jQuery中異步隊列的應用。

異步隊列

延遲對象(異步隊列)是在回調對象的基礎上實現的。這個延遲對象維護了三個列表:成功(done)回調函數列表,失敗(fail)回調函數列表和進行中(progress)回調函數列表,之所以有三個回調函數列表,是因爲延遲對象有三種狀態,分別是:resolve(成功), reject(失敗), notify(進行中)。
那麼延遲對象是如何在運用回調對象模塊來實現異步功能的?

看一個例子:

var dfd = $.Deferred();     //與回調對象類似,先定義一個延遲對象;

setTimeout( function(){   //當時間滿足一秒時
    dfd.resolve();      //相當於$.Callbacks().fire()方法,將加入到done(成功回調列表)中的函數執行,在這裏將彈出有'Yes'的窗口
}, 1000);

dfd.done(function(){    //相當於$.Callbacks().add(function(){})方法
//不同的是,這裏分別通過done和fail方法,將不同的回調函數添加到各自的回調列表中。
    alert('Yes');
}).fail(function(){  
    alert('No');       //如果setTimeOut中,將dfd.resolve()換成dfd.reject(),將彈出有'No'的窗口
})

根據上面分析,我們可以得到這樣一個對應關係:
這裏寫圖片描述

下來我們看看源碼中是如何實現的?

源碼框架

jQuery.extend({
    Deferred: function( func ){
        //三個回調函數列表
        doneList = $.Callbacks('once memory')
        failList = $.Callbacks('once memory')
        progressList = $.Callbacks('memory')
        //初始狀態
        state = 'pending'  //表示進行中

        //三種狀態和三個回調函數列表對應
        list = {
            resolve: doneList,
            reject: failList,
            notify: progressList
        }
        promise = {
            done: doneList.add,
            fail: failList.add,
            progress: progressList.add,
            state: function(){  },
            then: function(){  },    //依次添加doneList,failList和progressList回調函數
            always: function(){  },  //不管觸發resolve函數還是reject函數,都會執行該方法添加的回調函數
            pipe: function(){  },   
            promise: function(){  }  //返回promise對象的一個副本或者擴展的延遲對象
        }

        //重新建一個promise的副本
        deferred = promise.promise({});

        //給deferred延遲對象添加三種狀態函數
        for(key in list)
        {
            deferred[key] = list[key].fire;
            deferred[key + 'With'] = list[key].fireWith;
        }
        //執行deferred.resolve或者deferred.reject時,要改變初始的狀態,並且一旦狀態確定,便不會再更改,所以有下面的操作
        deferred.done(function(){
            state = 'resolved'
        }, failList.disable, progressList.lock).fail(function(){
            state = 'rejected'
        }, doneList.disable, progressList.lock);

        if( func ) { //Deferred也是支持參數的
            func.call(deferred, deferred);
        }

        return deferred;     //返回延遲對象
    }
})

在源碼中,我們可以清楚的看到,延遲對象是如何利用回調對象來實現回調函數的添加和執行的。除了這些,可以看到,在Deferred函數中,創建了一個promise對象,另外還又得到了一個deferred對象,這兩個對象之間有什麼區別和聯繫呢?

這裏寫圖片描述

可以看到,deferred對象由promise對象擴展而來,但是又比promise對象多了三個函數,這三個函數都是用於改變延遲對象狀態的。那麼爲什麼要用兩個延遲對象? 這是爲了保證外部不能對延遲對象的狀態進行改變。
舉個例子:

//函數a返回一個延遲對象
function a(){
    var dfd = $.Deferred();

    setTimeout( function(){
        dfd.resolve();
    }, 1000);

    return dfd;
}
//將a得到的延遲對象 賦值給一個新的延遲對象
var dfdNew = a().done(function(){
    alert("Yes");
}).fail(function(){
    alert('No');
});
//造成在函數外部,對延遲對象的狀態進行了改變。
dfdNew.reject();

由於上面的a函數返回的是一個延遲對象deferred,所有包含有resolve,reject和notify方法,因此可以在函數外部去改變延遲對象狀態,而此時我們是不希望外部對延遲對象的狀態進行改變,因此這才利用到promise對象。看下面段代碼:

function a(){
    var dfd = $.Deferred();

    setTimeout( function(){
        dfd.resolve();
    }, 1000);

    return dfd.promise();
}

var dfdNew = a().done(function(){
    alert("Yes");
}).fail(function(){
    alert('No');
});
//此時得到dfdNew延遲對象,並沒有resolve等改變狀態的方法,因此下面的語句會出錯。
dfdNew.reject();

這段代碼就按照我們的意願來執行了,外部並不能對延遲對象的狀態進行更改,只能通過a函數裏面的dfd.resolve來改變。

異步隊列的應用

在jQuery中,主要有兩個部分用到了異步隊列(延遲對象)模塊:

$.ajax$().ready()方法。

ready函數理解這篇博客中,整理了jQuery的ready方法,但當時還沒有學習關於回調對象,延遲對象的模塊,所以有一部分沒有深入,下來我們再來縷一縷ready函數。
主要看readyList.add(fn)這部分代碼,readyList是一個回調對象,$.Callbacks('once memory'),這就相當於Deferred對象中,成功回調函數列表、失敗回調函數列表,在jQuery.fn.ready函數中,將fn回調函數添加進回調列表中。
在頁面元素加載出來之後,觸發之前註冊的事件處理函數,這個事件處理函數中會去調jQuery.ready()方法,在這個方法中調readyList.fireWith()方法,從而實現回調函數的執行。

ajax模塊暫時還沒有看,所以後期整理時再回過頭來分析對於Deferred延遲對象的應用。

when方法

jQuery擴展了一個when方法,這個方法相當於是對Deferred對象的一個延伸,相當於對多個延遲對象組合起來進行處理。看下面一個例子:

function a(){
    var d = $.Deferred();
    d.resolve();
    return d;
}
function b(){
    var d = $.Deferred();
    d.resolve();
    return d;
}
$.when(a(), b()).done(function(){
    //當a()和b()同時返回一個resolve狀態的延遲對象時,纔會觸發doneList回調列表中的回調函數
    alert("Yes");
}).fail(function(){
    //當a()或者b()任意一個返回reject狀態的延遲對象,就會觸發failList列表中的回調函數 
    alert("No");
})

//而當參數不是Deferred對象或者無參數時,始終觸發doneList裏面的回調函數;並且可以通過arguments類數組訪問到傳的參數。
$.when(123, 8945).done(function(){
    alert("成功");
}).fail(function(){
    alert("失敗");
})

有上面分析和測試,可以想到,$.when()$.when(123, 456)是相同的處理;只有在參數爲延遲對象時,纔會判斷這些延遲對象的狀態,從而決定$.when後面添加的回調方法執行哪一個。

下來根據源碼縷一遍:

function( firstParam ) {
    //首先將參數轉換成數組,調用[].slice方法
    var args = sliceDeferred.call( arguments, 0 ),
    i = 0,
    length = args.length,   //得到參數的個數
    pValues = new Array( length ),  
    count = length,
    pCount = length,
    //根據參數個數,以及參數類型,設置deferred對象
    //當無參數或者多於一個參數時,deferred = $.Deferred()對象;
    //當且僅當參數是一個延遲對象類型時,deferred = 這個延遲對象
    deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? firstParam : jQuery.Deferred(),
    //deferred對象的一份拷貝,供外界使用,所以是利用promise方法得到,沒有resolve(),reject()以及notify()方法,以爲外界不允許對狀態再進行改變
    promise = deferred.promise();

    //下面這兩個函數先不看
    function resolveFunc( i ) {

    }
    function progressFunc( i ) {

    }
    //下面利用參數個數分情況處理
    if ( length > 1 ) {
        //多個參數時,又分當前參數是否爲延遲對象
        for ( ; i < length; i++ ) {
            //是延遲對象,分別向doneList列表,failList列表和progressList列表中添加回調。
            //因爲是延遲對象,即有'memory'標誌的回調對象,如果之前延遲對象的狀態已經確定,此時添加回調函數之後會立即執行回調函數。
            //並且可以看到,添加到failList中的回調函數是deferred.reject,也就是說只要參數中有一個延遲對象的狀態是reject,都會觸發最終deferred對象的reject。
            //而向doneList和progressList中添加的回調函數分別是resolveFunc和progressFunc,這兩個函數後面再分析。
            if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) {
                args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) );
            } else {
                //如果參數不是延遲對象,count減1.
                //這裏說明一下count的含義。when整體的實現思路,是用一個計數器來記錄是否所有的延遲對象都resolve或者都notify了。
                //初始值爲所有的參數個數,當參數不是延遲對象時,減一,當參數是延遲對象,且一直resolve或者一直notify,也減一。直到減爲0時,觸發deferred.resolve或者deferred.notify。
                --count;
            }
        }
        if ( !count ) {
            //count減爲0,觸發deferred.resolve().
            deferred.resolveWith( deferred, args );
        }
    } else if ( deferred !== firstParam ) {
        //無參數時,觸發deferred.resolveWith()
        //一個參數時,且參數不爲延遲對象時,同樣觸發deferred.resolveWith().
        deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
    }
    //一個參數,且參數爲延遲對象時,deferred = 這個延遲對象。
    //最後返回deferred.promise對象,也就是說,這個延遲對象的狀態是什麼,就去觸發$.when後面添加的回調函數即可。類似於一個延遲對象的處理。
    return promise;
}

下面來說resolveFuncprogressFunc這兩個方法:

//這兩個方法分別是添加進doneList和 progressList回調列表中的回調函數
function resolveFunc( i ) {
    //返回一個函數,這個函數每一次對計數器count減一,並且判斷在count爲0時,直接觸發deferred.resolve。
    return function( value ) {
        args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
        if ( !( --count ) ) {
            deferred.resolveWith( deferred, args );
        }
    };
}
//progressFunc與上面的函數是類似的,不同的是,這是對於progressList添加的回調函數
//更重要的不同點是: 只要有一個延遲對象的狀態爲notify,就都會觸發deferred.notify。
function progressFunc( i ) {
    return function( value ) {
        pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
        deferred.notifyWith( promise, pValues );
    };
}

到這裏,關於回調對象,延遲對象以及延遲對象的擴展when都已經分析完了。下來爲了加深對於這兩個對象的理解和運用,後面可能會從jQuery功能模塊的ajax方法來分析,期間對於回調對象和延遲對象的運用還會重點分析以加深理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章