jQuery原始碼閱讀(十三)---jQuery非同步佇列模組

鐺鐺鐺鐺Huan發表於2017-07-26

上一篇部落格分析了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方法來分析,期間對於回撥物件和延遲物件的運用還會重點分析以加深理解。

相關文章