聲明:寫博客,是對自身知識的一種總結,也是一種分享,但由於作者本人水平有限,難免會有一些地方說得不對,還請大家友善 指出,或致電:[email protected]
關注:國內開源jQuery-UI組件庫:Operamasks-UI
jQuery版本:v1.7.1
jQuery1.7系列三: jQuery延遲列表(Deferred)
一.函數列表
很多時候,我們想要執行一系列的函數,比如,當一個ajax請求後想要執行2個成功函數;當一個動畫結束後想要執行2個函數,那麼我們很容易想到用一個數組來承載這些待執行函數,如下所示:
var callbacks = [];
callbacks.push(function(){});
callbacks.push(function(){});
callbacks這個函數列表除了裝載函數外,沒有任何多餘的控制能力,在很多時候我們需要更加智能的函數列表。比如,它可以控制不可以添加重複的函數;可以控制何時開始觸發這些函數;可以控制當某個函數返回了false時後邊的函數將不再執行……
如果擁有了這樣強大的函數列表,那麼很多不可思議的功能將可以輕鬆的實現,而這一切都可以在jQuery中找到蹤跡。
二.jQuery加強的函數回調列表(Callbacks)
jQuery1.7提供了一個加強的函數回調列表,它不僅僅用於存放函數,最重要的是它提供了更加複雜的控制,如何時纔開始執行這些函數,這些函數是否可以重複。大體情況可以看下圖:
jQuery.Callbacks便是它的實現,比如,看個最簡單的代碼:
<script>
$(function($){
var callbacks = $.Callbacks();
callbacks.add(function(text){console.log("f1"+text);});
callbacks.add(function(text){console.log("f2"+text);});
//調用fire/fireWith纔會執行這些函數,並且可以傳遞參數
callbacks.fire("test");//輸出結果: f1test f2test
});
</script>
接下來,我們分別看下四個標準的控制標誌。
1. once
見名知義,只可以觸發一次函數列表,也就是隻有第一次調用fire/fireWith纔會生效。
<script>
$(function($){
//注意傳遞了參數 "once"
var callbacks = $.Callbacks("once");
callbacks.add(function(){console.log("f1");});
callbacks.fire();//輸出 "f1"
callbacks.fire();//什麼也不發生
callbacks.add(function(){console.log("f2");});
callbacks.fire();//還是什麼也不發生
//默認爲非"once"
var callbacks = $.Callbacks();
callbacks.add(function(){console.log("f3");});
callbacks.fire();//輸出f3
callbacks.fire();//輸出f3
});
</script>
2. memory
單看名字我覺得很難知道是什麼意思,它的作用是在add一個函數的時候,如果這時候函數列表已經全部執行完了,那麼剛剛add進去的函數會立即執行。
<script>
$(function($){
//注意傳遞了參數 "once"
var callbacks = $.Callbacks("memory");
callbacks.add(function(){console.log("f1");});
callbacks.fire();//輸出 "f1",這時函數列表已經執行完畢!
callbacks.add(function(){console.log("f2");});//memory作用在這裏,沒有fire,一樣有結果: f2
callbacks.fire();//再觸發一次,輸出 f1 f2
//與once一起使用
callbacks = $.Callbacks("once memory");
callbacks.add(function(){console.log("f3");});
callbacks.fire();//輸出 "f3",這時函數列表已經執行完畢!
callbacks.add(function(){console.log("f4");});//沒有fire,一樣有結果: f4
callbacks.fire();//由於爲"once",這裏將什麼也不執行
});
</script>
3. unique
函數列表中的函數是否可以重複,這個也很簡單。
<script>
$(function($){
var f1 = function(){console.log("f1");};
var callbacks = $.Callbacks();
callbacks.add(f1);
callbacks.add(f1);
callbacks.add(f1);
callbacks.fire();//輸出 f1 f1 f1
//傳遞參數 "unique"
callbacks = $.Callbacks("unique");
callbacks.add(f1); //有效
callbacks.add(f1); //添加不進去
callbacks.add(f1); //添加不進去
callbacks.fire();//輸出: f1
});
</script>
4. stopOnFalse
默認情況下,當fire函數列表的時候,整個列表裏邊所有函數都會一一執行,但如果設置了stopOnFalse,那麼當某個函數返回了false時,後邊的函數將不再執行。即使設置了memory,再次添加的函數也不會執行了。但如果沒有設置”once”,再次調用fire還是可以再次重新觸發的。
<script>
$(function($){
var f1 = function(){console.log("f1"); return false};//注意 return false;
var f2 = function(){console.log("f2");};
var callbacks = $.Callbacks();
callbacks.add(f1);
callbacks.add(f2);
callbacks.fire();//輸出 f1 f2
callbacks = $.Callbacks("memory stopOnFalse");
callbacks.add(f1);
callbacks.add(f2);
callbacks.fire();//只輸出 f1
callbacks.add(function(){console.log("f3");}); //不會輸出,memory已經失去作用了
callbacks.fire();//重新觸發,輸出f1
});
</script>
當然,Callbacks還提供了類似remove,disable,lock等方法,可以查看文檔,就不一一介紹了,最主要的還是上邊的核心思想以及四種控制其行爲的狀態標誌位。
三.延遲隊列(Deferred)
Deferred是一個延遲隊列,基於上邊提到的Callbacks,其實Callbacks本身就已經擁有延遲功能了(還記得我們要自己fire纔可以觸發函數列表吧)。而Deferred內部包含了三個Callbacks,它們的定義分別如下:
var doneList = jQuery.Callbacks( "once memory" ),
failList = jQuery.Callbacks( "once memory" ),
progressList= jQuery.Callbacks( "memory" ),
可見,doneList和failList只可以觸發一次,而progressList可以觸發多次,而這個Deferred可以用來作啥?我們先來看段代碼:
<script>
$(function($){
//xhr你可以認爲就是一個Deferred
var xhr = $.ajax({
url : "test.html",
//此success函數會添加到內部的doneList中去
success : function(){
console.log("success");
},
//此error函數會添加到內部的failList中去
error : function(){
console.log("error");
}
});
xhr.success(function(){
console.log("another success");
//既然success添加到doneList中去,而它又具有"once memory"標識,所以這裏添加進去的函數會馬上執行
xhr.success(function(){
console.log("aftersuccess");
});
}).error(function(){
console.log("anothererror");
});
//如果請求成功,輸出結果將是 "success" "anothersuccess" "after success"
//如果請求失敗,輸出結果將是 "error""another error"
});
</script>
其實,Deferred的設計剛好用在ajax上,在進行ajax請求的時候,我們往往要添加成功和失敗的回調處理,而且有時並不止一個,所以內部用了doneList和failList兩個Callbacks,至於progressList是個比較特殊的東西,它相當於一個預處理的功能,在doneList和failList觸發之前觸發,而且必須由用戶自己去觸發它,所以一般我們不用在意它。
那麼有了Deferred,ajax請求怎麼調用那些回調就顯而易見了,只要在ajax請求後根據狀態判斷是成功還是失敗,然後選擇觸發doneList或者failList就行了,沒什麼其它稀奇的事。
最後再講一下Deferred的promise方法,它是個挺好的概念來的,爲了更好的理解它,我們回到上邊的ajax實例。我們知道,doneList和failList應該由ajax內部來進行觸發,而你擁有的功能僅僅是加入函數而已,畢竟只有ajax自己才知道請求成功還是失敗了,那如果你拿到了上邊的xhr變量後偏偏要自己去 xhr.fire(),不就可以破壞ajax的回調處理了?
雖然我想作爲一個正常的開發者沒人會這樣去做吧,但jQuery作者還是留了後招,上邊雖然我註釋到 xhr 基本可以認爲是一個Deferred,但嚴格上說並不完全是,它是調用了Deferred.promise()返回的一個弱Deferred,此Deferred弱的地方就在於它沒有fire/fireWith這樣的方法,所以jQuery作者想得還是蠻周到的吧,當你想用Deferred開發自己的東西時,也許也會利用到這一個方法的。
總的說就是,Deferred.promise可以造出一個Deferred,但此Deferred只可以添加函數,不可以執行真正的觸發。
四.最後的話
jQuery的Deferred是一個設計挺巧妙的東西,如果你想實現自己的異步的隊列,完全可以參考它的設計,關於所要掌握的一些概念,在上邊都已經提到了,這只是一個初端,但對你進一步去了解它我覺得還是有好處的,特別是如果你想研究它的代碼實現,有了這些知識,就不會覺得非常難以理解了。
五. 附錄: Callbacks源碼註釋
/*
* Create a callback list using the following parameters:
*
* flags: an optional list of space-separated flags that will change how
* the callback list behaves
*
* By default a callback list will act like an event callback list and can be
* "fired" multiple times.
*
* Possible flags:
*
* once: will ensure the callback list can only be fired once (like a Deferred)
* 表示函數列表只有第一次fire()/fireWith()起作用
*
* memory: will keep track of previous values and will call any callback added
* after the list has been fired right away with the latest "memorized"
* values (like a Deferred)
* 在調用add()添加函數時,如果函數列表已經觸發過並執行完畢,只要設置了此值,新添加進去的函數將馬上執行,否則不執行
*
* unique: will ensure a callback can only be added once (no duplicate in the list)
* 設置了此值表示函數列表中的函數不可以重複(兩個不同引用指向同個函數意爲重複)
*
* stopOnFalse: interrupt callings when a callback returns false
* 設置了此值,一旦某個函數返回了false,其後的函數將不會執行,但如果flags.once=false,那麼再次
* 調用fire還是可以再次觸發的
*
*/
//flags的值比空白隔開 ,比 "once memory" "unique stopOnFalse"
jQuery.Callbacks = function( flags ) {
// Convert flags from String-formatted to Object-formatted
// (we check in cache first)
//最終flags將會是一個對象,比如{once:true , unique:true}
flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {};
var // Actual callback list
//存放函數的數組
list = [],
// Stack of fire calls for repeatable lists
//如果flags.once=true,那麼當你調用fire時,而且這時上一輪的fire還沒有執行完畢,這時候將會把 [context , args]入棧
//然後等上一次fire完結後,它會繼續判斷stack裏邊是否還有值,有的話會拿出[context , args]再次執行所有的函數列表
stack = [],
// Last fire value (for non-forgettable lists)
//默認值爲false,表示此函數列表還沒有觸發過,一旦此函數列表觸發過了,將有兩種可能的情況:
//如果flags.once=true,那麼memory=true,以後在fire時,一發現memory=true,將什麼也不做
//如果flags.memory=true,那麼memory=[context , args]
memory,
// Flag to know if list is currently firing
//是否正在觸發中
firing,
// First callback to fire (used internally by add and fireWith)
//從函數列表哪裏開始執行
firingStart,
// End of the loop when firing
//執行的函數列表長度,也就是執行函數的個數
firingLength,
// Index of currently firing callback (modified by remove if needed)
firingIndex,
// Add one or several callbacks to the list
//真正執行添加函數的內部方法
add = function( args ) {
var i,
length,
elem,
type,
actual;
for ( i = 0, length = args.length; i < length; i++ ) {
elem = args[ i ];
type = jQuery.type( elem );
//傳遞了數組同樣可以處理
if ( type === "array" ) {
// Inspect recursively
add( elem );
} else if ( type === "function" ) {
// Add if not in unique mode and callback is not in
//處理flags.unique,如果flags.unique=true,還要判斷一下是否函數已經存在了
if ( !flags.unique || !self.has( elem ) ) {
list.push( elem );
}
}
}
},
// Fire callbacks
//真正執行觸發函數列表的方法
fire = function( context, args ) {
args = args || [];
//默認情況下,flags.memory=false,這時memory=true,表示已經執行過了
//如果flags.memory=true,memory=[context , arsg]
memory = !flags.memory || [ context, args ];
firing = true;
//下邊爲什麼要這麼麻煩呢,用三個參數來控制.
//在add的時候,如果flags.memory=true,並且函數列表執行過了,這時候只要執行新的函數就行了,
//而在add中會設置firingStart爲合適的值,這樣自然就只執行最後添加的函數了
firingIndex = firingStart || 0;
firingStart = 0;
firingLength = list.length;
for ( ; list && firingIndex < firingLength; firingIndex++ ) {
//這是唯一處理flags.stopOnFalse的地方
if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) {
//只要flags.stopOnFalse=true,並且函數返回了false,那麼memory強制設爲true,這時候flags.memory將會失去作用
//因爲flags.memory=true的作用就是 設置memory=[context , args]
memory = true; // Mark as halted
break;
}
}
firing = false;
//當觸發執行完畢後,還要檢查一下stack的情況,處理flags.once=false,並且調用了多次的fire()
if ( list ) {
if ( !flags.once ) {
if ( stack && stack.length ) {
memory = stack.shift();
//memory=[context , args]在這裏將用到了
self.fireWith( memory[ 0 ], memory[ 1 ] );
}
} else if ( memory === true ) {
//既然已經觸發過了,並且沒有額外的控制,如沒有設置flags.once=false,沒有設置flags.memory=true,那麼作廢該 Callbacks吧
self.disable();
} else {
list = [];
}
}
},
// Actual Callbacks object
//這就是調用 jQuery.Callbacks()返回的對象,用戶眼中的Callbacks實例
self = {
// Add a callback or a collection of callbacks to the list
add: function() {
//只要未曾 disable此Callbacks,此條件將永遠爲true
if ( list ) {
var length = list.length;
add( arguments );//調用真正的函數添加處理
// Do we need to add the callbacks to the
// current firing batch?
//如果當前Callbacks正在觸發,那麼只要修改firingLength的值就可以了,這樣剛添加進去的函數自然就會執行了
if ( firing ) {
firingLength = list.length;
// With memory, if we're not firing then
// we should call right away, unless previous
// firing was halted (stopOnFalse)
//在添加的時候,只有一種情況會觸發函數列表,那就是flags.memory=true,而且
//如果flags.stopOnFalse=true,並且有函數返回了false,那麼即使
//flags.memory=true,這時候memory也會被強制設置爲true,flags.memory完全失去了作用。
} else if ( memory && memory !== true ) {
firingStart = length;
fire( memory[ 0 ], memory[ 1 ] );
}
}
return this;
},
// Remove a callback from the list
//有添加,自然也可以刪除函數
remove: function() {
if ( list ) {
var args = arguments,
argIndex = 0,
argLength = args.length;
for ( ; argIndex < argLength ; argIndex++ ) {
for ( var i = 0; i < list.length; i++ ) {
//找到要刪除的函數了,這時候要看Callbacks是否真在執行,若真在執行,要修改一些內部狀態
if ( args[ argIndex ] === list[ i ] ) {
// Handle firingIndex and firingLength
if ( firing ) {
if ( i <= firingLength ) {
firingLength--;
//firingIndex表示正在執行函數的索引,如果
//要刪除的函數在這個索引前面,說明firingIndex必須減1,然後把前邊那個函數刪了
if ( i <= firingIndex ) {
firingIndex--;
}
}
}
// Remove the element
list.splice( i--, 1 );
// If we have some unicity property then
// we only need to do this once
//如果函數是唯一的,那麼重新開始下一個函數的刪除
if ( flags.unique ) {
break;
}
}
}
}
}
return this;
},
// Control if a given callback is in the list
//一個給定函數是否已經在函數列表中了
has: function( fn ) {
if ( list ) {
var i = 0,
length = list.length;
for ( ; i < length; i++ ) {
if ( fn === list[ i ] ) {
return true;
}
}
}
return false;
},
// Remove all callbacks from the list
//純粹的清空而已,不影響函數列表的添加和觸發
empty: function() {
list = [];
return this;
},
// Have the list do nothing anymore
//一經disable,這個函數列表就完全不起作用了,相當於廢了,可以休息了
disable: function() {
list = stack = memory = undefined;
return this;
},
// Is it disabled?
//判斷函數列表是否已經disable了
disabled: function() {
return !list;//list==undefined就說明該函數列表廢了
},
// Lock the list in its current state
//一旦上了鎖,stack=undefined,那麼即使flags.once=false,調用再多的fire也是無濟於事,其實Callbacks也相當於廢了
lock: function() {
stack = undefined;
if ( !memory || memory === true ) {
self.disable();
}
return this;
},
// Is it locked?
locked: function() {
return !stack;
},
// Call all callbacks with the given context and arguments
fireWith: function( context, args ) {
if ( stack ) {
if ( firing ) {
if ( !flags.once ) {
//如果當前Callbacks正在觸發,你這時又調用了一次fire,這時候只要flags.once=false,那麼就會把[context , args]入棧
//這樣當上一次的觸發完成時,將會檢查stack,然後入棧自動重新執行函數列表
stack.push( [ context, args ] );
}
} else if ( !( flags.once && memory ) ) {
//把條件轉換成 !flags.once || !memory感覺好懂一些
//如果flags.once=false,那麼繼續觸發吧
//或者flags.once=true,但 memory=false,表示還沒觸發過,那麼執行第一次的觸發吧
fire( context, args );
}
}
return this;
},
// Call all the callbacks with the given arguments
fire: function() {
self.fireWith( this, arguments );
return this;
},
// To know if the callbacks have already been called at least once
//memory=false表示還沒有觸發,其它值均表示觸發過了
fired: function() {
return !!memory;
}
};
return self;
};