jQuery1.7系列三: jQuery延遲列表(Deferred)

           聲明:寫博客,是對自身知識的一種總結,也是一種分享,但由於作者本人水平有限,難免會有一些地方說得不對,還請大家友善  指出,或致電:[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;
};

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章