jQuery異步框架探究1:jQuery._Deferred方法


jQuery異步框架應用於jQuery數據緩存模塊、jQuery ajax模塊、jQuery事件綁定模塊等多個模塊,是jQuery的基礎功能之一。實際上jQuery實現的異步回調機制可以看做java nio(不是aio)的近似,所以需要從更抽象層面的"異步回調"的視角分析解讀該模塊。這個部分與dom功能關系不大,是獨立部分,可以看作是jQuery工具系列之一。

與異步框架相關的方法定義於jQuery類的靜態方法中。只有三個方法,但是功能和應用及其強大!本篇詳細講解第一個方法jQuery._Deferred()。


_Deferred: function() {
var // callbacks list
callbacks = [],
// stored [ context , args ]
fired,
// to avoid firing when already doing so
firing,
// flag to know if the deferred has been cancelled
cancelled,
// the deferred itself
deferred = {

// done( f1, f2, ...)
done: function() {
if ( !cancelled ) {
var args = arguments,
i,
length,
elem,
type,
_fired;
if ( fired ) {
_fired = fired;
fired = 0;
}
for ( i = 0, length = args.length; i < length; i++ ) {
elem = args[ i ];
type = jQuery.type( elem );
if ( type === "array" ) {
deferred.done.apply( deferred, elem );
} else if ( type === "function" ) {
callbacks.push( elem );
}
}
if ( _fired ) {
deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
}
}
return this;
},

// resolve with given context and args
resolveWith: function( context, args ) {
if ( !cancelled && !fired && !firing ) {
// make sure args are available (#8421)
args = args || [];
firing = 1;
try {
while( callbacks[ 0 ] ) {
callbacks.shift().apply( context, args );
}
}
finally {
fired = [ context, args ];
firing = 0;
}
}
return this;
},

// resolve with this as context and given arguments
resolve: function() {
deferred.resolveWith( this, arguments );
return this;
},

// Has this deferred been resolved?
isResolved: function() {
return !!( firing || fired );
},

// Cancel
cancel: function() {
cancelled = 1;
callbacks = [];
return this;
}
};

return deferred;
},

這是jQuery異步框架最基礎的一個函數,真正對外暴露的Deferred函數依賴該函數的實現,從其名字可以看出,jQuery的設計者並不想使用者直接調用該方法,雖然這個方法已經很靈活已經可以應用到很多場景了,而且javascript語言也沒有語言層面上的機制阻止用戶真的調用該方法。

分析其內部實現可以發現該函數設計為在被調用時並不需要傳入任何參數,而是直接調用后得到一個異步對象:var deferred = jQuery._Deferred()。重點是這個返回的異步對象,這個對象有五個方法:done、resolveWith、resolve、isResolved、cancel,每個方法都值得深入分析。當然_Deferred函數里面還有四個變量:callbacks、fired、firing、cancelled,這四個變量也值得深入分析。

1 done方法


這個方法的實現是把傳入的參數push到callbacks空數組中,以便后續resolveWith方法使用callbacks數組中的函數。

這個方法的參數只接受兩種類型:要么是函數,要么是函數數組,其他類型不被處理。且傳入函數數組時遞歸調用done方法自身繼續把數組中的函數元素push到callbacks數組中。

配合resolveWith方法可以知道done方法的主要功能是存儲將要執行的函數集合,resolveWith方法的功能是在指定的對象上根據指定的參數對象依次調用這個函數集合。因此,callbacks函數集合相當於是回調處理器集合,fired = [ context, args ]相當於是調用對象(包括其參數)。這樣分離后帶來的靈活性是驚人的--異步處理框架即將成型。順便說一下,這個方法的名字done非常的不友好,對於熟悉回調模式的朋友們來說,其實這個方法名應該是addHandlers或者addCallbacks,甚至返回對象deferred(延遲)的名字都不如asynchor來得直觀。

這個方法最后返回this關鍵字,即deferred對象自身,這樣做的好處是可以鏈式多次調用,比如jQuery._Deferred().done(f1,f2).done([f3,f4,f5]),如果deferred對象的其他方法也這么干的話,那么也可以鏈式調用其他方法,而其他方法正是這么干的。

關於_fired變量和相關代碼的作用在分析完resolveWith方法之后再看才會明朗。

2 resolveWith方法


resolveWith方法的實現是迭代回調函數集合callbacks中的函數於指定對象context和參數args之上執行--如果callbacks不為空的話,並且最后總是把指定對象context和參數args存入fired變量中--有可能還沒有執行回調,如果callbacks為空。

resolveWith方法先判斷回調函數集合callbacks是否有值"callbacks[ 0 ]",沒有的話什么也不觸發。

resolveWith方法最核心的是這一行:callbacks.shift().apply( context, args )。除了從回調函數集合callbacks中取出第一個函數元素調用外它還移除了這個元素,改變了callbacks數組,這樣保證回調函數集合callbacks中的每個元素僅能執行唯一一次。

對於軍事迷來說,有一個模型與這里的_Deferred函數極為類似,幾乎再也找不到更貼切的類比了--手槍。
縱觀_Deferred函數,回調函數數組callbacks相當於彈夾;
傳遞給done方法的函數參數相當於將要存儲於彈夾的子彈,done方法主要職責是提供"壓彈入夾"操作;
傳遞給resolveWith方法的參數( context, args )相當於手槍,resolveWith方法主要職責是執行"開槍"操作;
fired變量相當於槍套,存儲傳遞給resolveWith方法的手槍[ context, args ];

現在回過頭再看done方法,done方法是與resolveWith配合使用的,一般情況下既可以先上彈后開槍--先調用done方法后調用resolveWith方法,這種場景下手槍存在前就已經備有子彈了,因此直接在調用resolveWith方法時觸發"開槍"操作;但是也可以先備槍后上彈射擊--先調用resolveWith方法后調用done方法,這種場景在傳遞手槍的時候還沒有子彈,提供子彈后才去觸發射擊動作--done方法內部調用resolveWith方法:

if ( fired ) {
_fired = fired;
fired = 0;
}
<span style="white-space:pre"></span>// ......
if ( _fired ) {
deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
}


除了前面兩種操作外還有一種特殊操作:上彈-->開槍-->再上彈開槍(先調用done方法再調用resolveWith方法后再調用done方法),這個時候done方法裝上新彈(傳入新的函數參數push到回調函數數組)后可重復開槍(繼續內部調用resolveWith方法),如果不換上新彈(不傳入新的函數參數)就不能重復開槍(原回調函數數組經過resolveWith方法調用一次后已清空,它不可重復使用),這個操作時序完美的模擬手槍空倉掛機復位射擊功能(子彈打光時阻止套筒回位,更換彈夾后可快速推彈入膛)!

這行代碼"fired = 0;"的作用非常重要,正因為有這行代碼才可以在done方法調用resolveWith方法重復開槍時生效(因為resolveWith方法內的第二個判斷條件"if ( !cancelled && !fired && !firing )",我們前面說過fired變量表示槍套,怎么理解呢?關於槍套變量fired也需要重點解釋,參見下面詳細解釋fired的部分。

我們再來看一個擴展的操作:jQuery._Deferred().done(f1,f2).resolveWith(context1, args1).done(f3).done([f4]).done(f5,[f6,f7]),說明空倉掛機復位射擊的確是可重復的!這個世界上應該沒有哪位槍械師會設計不能重復使用或只能重復使用一次的手槍吧?

關於這兩個方法總結一下針對同一把槍的使用方法:
jQuery._Deferred().done(f1,f2).resolveWith(context1, args1)--先上彈后造槍,擊發一次。
jQuery._Deferred().resolveWith(context1, args1).done(f1,f2)--先造槍后上彈,擊發一次。
jQuery._Deferred().done(f1,f2).resolveWith(context1, args1).done(f3,f4).done([f5,f6],f7)--先上彈后造槍射擊一次,再空倉掛機復位射擊兩次(可重復更多次射擊)。

關於這兩個方法即使做了總結之后也並沒有完,注意前面的一個提示性描述:針對同一把槍,我們已經知道同一把槍可以重復使用不同的子彈,但是如果想針對不同的槍使用相同的子彈呢?這個需求是合理的--成員國眾多的北約內部,標准9mm巴拉姆彈可不是只能應用於沙漠之鷹。

"針對不同的槍"說明需要調用resolveWith方法至少兩次,第一次調用resolveWith方法傳入的參數(context1, args1)代表第一把槍,第二次傳入的參數(context2, args2)代表第二把槍,我們看看會發生什么情況:"jQuery._Deferred().done(f1,f2).resolveWith(context1, args1).resolveWith(context2,args2);"很遺憾,這個調用會歇菜!還是因為槍套變量fired的原因。變通一下呢:"jQuery._Deferred().done(f1,f2).resolveWith(context1, args1).done(f3).resolveWith(context2, args2)",還是不行!同樣因為槍套變量fired的原因,resolveWith方法能且僅能調用一次,它不像done一樣可以鏈式調用多次。

除了因為槍套變量fired的原因外,即使排除這個原因,"不同的槍使用相同的子彈"也是不能實現的,槍對子彈是一對多的關系,子彈對槍卻是一對一的關系,即回調函數數組中的每一顆子彈被一只手槍擊發后並不能被另一只槍再次擊發,真正合理的需求描述是"不同型號的槍支應該可以使用相同規格的子彈"。

所以結論是--jQuery._Deferred()不能針對相同的子彈使用不同的手槍,它每次上彈后就認為只能匹配給第一次傳入的手槍(或者說第一次造槍后就只能使用這種型號的這只手槍),如果要使用不同的手槍怎么辦呢?--多次調用jQuery._Deferred()方法即可。

3 槍套--變量fired


首先需要熟悉的一個基礎知識點是:done方法和resolveWith方法操作的變量fired是在函數調用對象鏈上一級的調用對象中,其他三個變量都是如此,這意味着deferred對象的不同方法可以共同操作它們。

resolveWith方法內有一行判斷語句:"if ( !cancelled && !fired && !firing )",這三個變量任何一個都可能導致不能"開槍",關於firing和cancelled參見下面詳細分析,這里分析fired變量。如果這個槍套不是有槍才能執行開槍操作,或者簡單說只有這個槍套是空的才能執行開槍(其實同時受firing和cancelled約束):槍套不空說明里面存儲手槍了,手槍沒取出來當然不能開槍--實際上即使此時傳入新的手槍進來也不能使用(jQuery._Deferred()對象比較重感情,它只使用第一次接觸到的手槍,這也充分說明第一次是多么的重要。)!resolveWith方法在finally塊中賦值給變量:"fired = [ context, args ];",相當於使用結束后將手槍插入手槍套,

再看done方法對fired變量的操作:"if ( fired ) {_fired = fired;fired = 0;}",槍套中有槍時,把它取出來存入當前調用對象域中的臨時變量_fired,此時一定要清空槍套!否則下面調用resolveWith方法會失效(resolveWith方法會先檢查槍套情況)。

4 射擊指示器--變量firing


這個變量相當於"射擊指示器",作用是當前正在射擊時(調用resolveWith方法並通過了條件判斷)給手槍標記一個狀態--"正在射擊"的狀態("firing = 1;"),這個狀態下的手槍不能再次扣動扳機開槍(即不能再次調用resolveWith方法),除非當前這次射擊結束(在finally塊中清除這個狀態"firing = 0;")。

如果用手槍上的部件來類比的話,有點兒像"扳機連桿突起",作用是一樣的:擊發前扳機連桿鈎住阻鐵可以執行當前射擊,擊發后扳機連桿脫鈎阻鐵,同時套筒在后座作用下通過"扳機連桿突起"強迫扳機連桿在射擊過程中不能自動回位鈎着阻鐵,直到子彈擊發出去后套筒復進到位本次射擊結束才放開扳機連桿,這樣就保證了射擊過程中不會重復執行擊發動作。當然並不是所有手槍都通過設計"扳機連桿突起"完成射擊指示功能的。

這個變量在實際執行環境中應該過於嚴格了,除非javascript執行器采用的是多線程執行環境並且不同線程對這個變量的訪問可能出現race condition,否則我認為單線程環境下應該不需要這個變量(也可能是我理解有誤)。

5 全能保險--變量cancelled和cancel方法


cancelled變量非常類似手槍上的"擊發保險"+"彈夾防跌落保險",如果不是需要調用cancel方法顯式設置的話,那么它也可以類似"擊針保險",總之它是一個全能保險。
關閉保險之后("cancelled = 1;"),resolveWith方法無法開槍了(判斷條件不通過),就像擊發保險被鎖死一樣。
關閉保險之后("cancelled = 1;"),done方法無法壓彈入匣了(判斷條件不通過),就像彈夾扣被鎖死一樣。
調用cancel方法在關閉保險的同時也清空彈夾了("callbacks = [];")。
總之調用cancel方法之后整個系統都不好了,大部分地方都不能繼續正常工作了。


6 resolve方法


這個方法調用resolveWith方法,不需多說。


7 isResolved方法


只要射擊指示器firing亮着或者槍套fired里有槍就認為已resolved。

總之,如果你是一個對手槍模型工作原理十分清楚的軍事迷,那么讀懂jQuery._Deferred源碼和原理幾乎是天然的。jQuery._Deferred只是jQuery異步框架的第一個最基礎的部分就已經展開如此多的篇幅了,但是因為它是jQuery異步框架的核心,也是后兩個方法的基礎,這種展開是值得的。


最后鄭重申明一點:未經許可,嚴禁轉載!尊重他人勞動成果是獲得對等尊重的前提。



注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com