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互相調用。