天天看點

jQuery2.x源碼解析(回調篇)

jQuery2.x源碼解析(建構篇) 

jQuery2.x源碼解析(設計篇) 

jQuery2.x源碼解析(回調篇) 

jQuery2.x源碼解析(緩存篇) 

通過艾倫的部落格,我們能看出,jQuery的promise和其他回調都是通過jQuery.Callbacks實作的。是以我們一起簡單看看jQuery.Deferred和jQuery.Callbacks。來看看關于他們的一些提問。

提問:jQuery.Callbacks的配置為什麼是用字元串參數?

jQuery.Callbacks有四種配置,分别是once、memory、unique、stopOnFalse。而jQuery.Callbacks的配置形式卻和以往我們熟悉的不同,不是使用json,而是使用字元串的形式配置的,這是為什麼呢?

答:jQuery.Callbacks的配置形式,确實很怪異,jQuery.Callbacks使用了一個createOptions函數将字元串轉為了json。

var rnotwhite = ( /\S+/g );
function createOptions( options ) {
    var object = {};
    jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
        object[ flag ] = true;
    });
    return object;
}      

如"once memory unique"字元串,最終會轉為{"once":true,"memory":true,"unique":true}。但是筆者想問為什麼不直接使用json呢?

像這種使用字元串配置的形式,JavaScript中也是有的,js中的正規表達式就是這種配置形式。

如:

var patt = new RegExp("e","gim");      

這裡的“gim”就是配置項。但是這種配置方法很難讓人了解,這樣不符合jQuery追求的理念,是以筆者認為jQuery.Callbacks使用字元串配置代替數組配置是jQuery裡面的一個敗筆。

提問:jQuery.Deferred封裝的函數一定是異步的嗎?

答:這是筆者以前一直疑惑的一個問題。我們先跳出jQuery,先使用标準的JavaScript,看看他的異步函數一定是異步的嗎?

測試代碼如下:

setTimeout(function(){
    console.log(0);
},0);
setTimeout(function(){
    console.log(1);
},0);

console.log(2);      

非常簡單的一行代碼,我們也很清楚輸出結果是“201”。JavaScript是單線程執行的,是以異步函數的回調會在後邊執行。同時異步的回調函數會被放入隊列中,是以會按照進入隊列的順序執行下來。注意setTimeout的時間參數一定要給0,因為setTimeout的回調時間預設值不一定是0。

我們把中間的輸出1的函數的setTimeout去除,讓其不再異步。

setTimeout(function(){
    console.log(0);
},0);
console.log(1);
console.log(2);      

結果變為了“120”。這個結果不需要作說明,僅是為了做對比而做的實驗。

現在換位使用異步函數的文法,将輸出1的部分用es7的異步函數包裹:

async function test(){
  await Date.now(); //注意test執行的時候如果不給await修飾,test雖然是一個異步函數,但是在異步部分是沒有可執行代碼的。console.log(1)會在同步部分執行
  console.log(1); 
} 

setTimeout(function(){ 
  console.log(0); 
},0); 
test();

console.log(2);      

結果是“210”。我們知道上面的Date.now并不是一個異步函數,但是console.log(1)還是在console.log(2)執行,說明await後面的代碼是在異步回調中運作的。

我們再将異步函數轉為promise。

setTimeout(function(){

  console.log(0);

},0);

new Promise(function(resolve){

  resolve();

}).then(()=>{

   console.log(1)

})

console.log(2);

上述代碼應該和異步函數結果相同,結果也确實相同,結果是“210”。

從這些例子我們可以看出,使用promise封裝的函數(異步函數),其回調部肯定是異步回調。

現在我們用jQuery.Deferred封裝的promise再将上面的實作進行一次。

setTimeout(function(){
  console.log(0);
},0);
$.Deferred().resolve().then(function(){
    console.log(1);
});
console.log(2);      

如上的結果是“120”。這說明jQuery的promise和标準的promise的表現形式是不一樣的。jQuery.Deferred的回調,如果resolve不是在異步回到中執行的,那麼then裡面的回調也不會在異步回調中執行,這與标準的promise是不同的。

我們也可以看看jQuery.Callbacks源碼,裡面是不含有setTimeout,或者其他可以傳回異步回調的。不過這是jQuery2.0的結果,jQuery3.0的結果是“201”,有興趣的話大家可以自己嘗試。

提問:jQuery.Callbacks的回調中的this指向什麼?

答:簡單的分析一下源碼結構:

jQuery.Callbacks = function( options ) {
    
    var list = [];
    var fire = function() {
         list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) ;
    }
    
    return  {
        add: function() {
            ( function add( args ) {
                jQuery.each( args, function( _, arg ) {
                    list.push( arg );
                } );
            } )( arguments );

                
            return this;
        },

        fireWith: function( context, args ) {
            args = [ context, args.slice ? args.slice() : args ];
            queue.push( args );
            if ( !firing ) {
                fire();
            }
            
            return this;
        },

        fire: function() {
            self.fireWith( this, arguments );
            return this;
        },
    };
};      

從這段代碼可以清楚看到,通過add我們将函數儲存在内部私有變量list裡面,然後使用apply調用。對外暴露的函數有fireWith和fire。fireWith的context參數是最終傳遞給了apply,是以是我們回調中的this就是這個context。而fire函數裡面調用了fireWith,傳遞的是自身this,是以回調的函數中的this是Callbacks對象。

提問:jQuery.Deferred的回調中的this指向什麼?

答:标準的promise中的this,是指向全局作用域的,例如window。

var d = new Promise((r)=>{r()})
d.then(function(){console.log(this)})  //注意,此處不可以用()=>{console.log(this)}      

輸出的是window,證明列我們之前的說法。

我們再來看看jQuery.Deferred的示意源碼:

jQuery.extend( {

    Deferred: function( func ) {
        var tuples = [
                [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ],
                [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ],
                [ "notify", "progress", jQuery.Callbacks( "memory" ) ]
            ],
            state = "pending",
            promise = {
                then: function( /* fnDone, fnFail, fnProgress */ ) {
                    var fns = arguments;
                    return jQuery.Deferred( function( newDefer ) {
                        jQuery.each( tuples, function( i, tuple ) {
                            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()
                                        .progress( newDefer.notify )
                                        .done( newDefer.resolve )
                                        .fail( newDefer.reject );
                                } else {
                                    newDefer[ tuple[ 0 ] + "With" ](
                                        this === promise ? newDefer.promise() : this,
                                        fn ? [ returned ] : arguments
                                    );
                                }
                            } );
                        } );
                        fns = null;
                    } ).promise();
                },

                promise: function( obj ) {
                    return obj != null ? jQuery.extend( obj, promise ) : promise;
                }
            },
            deferred = {};


        jQuery.each( tuples, function( i, tuple ) {
            var list = tuple[ 2 ],
                stateString = tuple[ 3 ];

            promise[ tuple[ 1 ] ] = list.add;

            if ( stateString ) {
                list.add( function() {

                    state = stateString;

                }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
            }

            deferred[ tuple[ 0 ] ] = function() {
                deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments );
                return this;
            };
            deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
        } );

        promise.promise( deferred );

        if ( func ) {
            func.call( deferred, deferred );
        }

        return deferred;
    }
} );      

代碼中建構了兩個對象——deferred和promise。deferred相當于對異步過程的封裝,而promise是對promise模型的封裝。代碼的最後将promise織入到deferred中。

resolve、reject、notify是promise的三種設定狀态的函數,實際上這三個函數的執行政策相似,是以jQuery采用政策模式,用一個二維數組tuples将這3種政策封裝起來。

代碼下半部分對tuples的周遊,我們可以看到,jQuery實作了promise的done、fail、progress這三個函數,這三個函數實作方式都是對jQuery.Callbacks.add的封裝。對deferred擴充了六個函數——resolve、reject、notify和resolveWith、rejectWith、notifyWith,前三個函數是對後三個函數的封裝,後三個函數都是對jQuery.Callbacks.fireWith封裝的。前三個函數調用後三個函數的時候,context參數都是傳遞的promise(除非回調函數被用bind、reject、resolve改變了上下文)。是以當deferred執行resolve、reject、notify的時候,回調函數的this是promise對象;當this使用resolveWith、rejectWith、notifyWith,或者用bind、call、apply改變了resolve、reject、notify的執行上下文的時候,回調的this是指向給定的對象。

再看上邊then方法的定義,同樣是周遊tuples。tuples數組定義的done、fail、progress這三個函數名稱,與then的三個參數一緻(done、catch、notify),這三個參數都是回調函數。回調他們的方式是deferred的done、fail、progress三個函數,我們知道這三個函數是使用promise織入進來的,真正的方法是promise的resolve、reject、notify這三個函數,是以then中的回調的this也是指向的是promise或者之前被指定的上下文。

this指向promise其實并沒有太多意義,而通過xxxWith函數或者用bind、call、apply改變了resolve、reject、notify的上下文的方式調用,才是jQuery的亮點。jQuery提供這樣的api的目的是為了我們可以指定promise的this,這樣貌似更靈活,更友善我們操作回調。

但是,回調中this的不同,是jQuery.Deferred和标準promise一個很大的差別,這是不标準的用法,這一點一定要切記。jQuery提供的api雖然很友善,但是這樣改變列promise模型,是不推薦的用法,尤其是promise如今已經收納到es6的文法中,es7的異步文法也是基于promise的,在不支援promise的浏覽器上建立出标準的promise才是jQuery更該做的,因為隻有這樣才能實作promise文法的對接。

提問:為什麼将promise織入到deferred中?

答:按照jQuery的思路,deferred相當于對異步過程的封裝,是promise的建立者與指揮者,但是根據jQuery的思路,将二者統一能更好的簡化異步對象的模型,如:

var d = $.Deferred();
d.resolve();
d.promise().then(()=>{
    ...
})

//等效于
var d = $.Deferred();
d.resolve()
.then(()=>{
    ...
})      

下邊的寫法是不是更加簡單緊湊呢?同時也符合jQuery的鍊式操作。簡單來說,少了一個對象的概念,大家當然更容易了解。

事實上,promise還可以織入到其他對象中,如:

var d = $.Deferred();
d.resolve();
d.promise(myObj);
myObj.then(()=>{
   ...     
})      

通過這種方式,jQuery可以很靈活的把promise的操作嵌入到任何對象中,非常友善。

提問:jQuery.Deferred.promise()有沒有實作promises/A+嗎?

答:是的,這個無需看源碼,僅看api也很清楚,jQuery.Deferred.promise()沒有實作promises/A+。例如jQuery增加了done、fail、progress等函數,這三個函數都不是promises/A+模型标準的函數名,但是卻近似實作了promises/A+的概念模型。同時,promises/A+裡面有catch函數,但是jQuery卻沒有實作,主要原因是catch是早期的ie浏覽器中的關鍵字,是以使用了fail代替。done、progress是jQuery根據自身需要進行的擴充。

jQuery.Deferred.promise和promises/A+還有一個重要差別,就是對異常的處理。執行個體代碼如下:

d = new Promise((resolve)=>{resolve()})
d.then(()=>{throw "error"})
.catch((e)=>{alert(e)})      

标準的promise彈出error字樣的alert框。

再換位jQuery.Deferred.promise測試:

$.Deferred().resolve().then(()=>{throw "error"}).fail((e)=>{alert(e)})      

結果直接報錯,并沒有彈出alret框。

源碼中jQuery沒有對回調的調用做異常處理,是以無法把回調的異常自動傳回rejected的promise。這一點不同使得jQuery.Deferred的異常處理非常不靈活,需要手動進行。修改為:

$.Deferred()
.resolve()
.then(()=>{
    try{
        throw "error"
    }catch(e){
        return $.Deferred().reject(e);
    }
}).fail((e)=>{
    alert(e)
})              

這樣才能彈出error字樣的alert框。

提問:jQuery.Deferred.promise()能和浏覽器的promise互相調用嗎?

答:當然不能直接調用,需要做轉換,轉換代碼這裡就不示範了。不能直接調用的原因是jQuery.Deferred.promise沒有實作promises/A+。但是這隻是一部分原因,最大的原因還是曆史問題,jQuery2.0的時候,浏覽器的promise模型還沒有建立,是以根本不可能去考慮與浏覽器的promise互相調用的問題。

提問:如果想要jQuery.Deferred.promise()能和浏覽器的promise直接調用,jQuery.Deferred.promise需要做哪些改善?

答:首先要對promises/A+做到完整實作,去掉jQuery個性化的東西,如xxxWith調用、Deferred代替promise、done等不規範的函數命名。是以,在jQuery3中jQuery.Deferred做了不可更新式的調整,新的jQuery.Deferred.promise提供了一套新的api,完全實作了promises/A+規範,并且可以調用浏覽器的promise互相調用。