jquery源碼解析:jQuery隊列操作queue方法實現的原理


我們先來看一下jQuery中有關隊列操作的方法集:

從上圖可以看出,既有靜態方法,又有實例方法。queue方法,相當於數組中的push操作。dequeue相當於數組的shift操作。舉個例子:

function aaa(){

  alert(1);

}

function bbb(){

  alert(2);

}

$.queue(document,"q1",aaa);   //在document下創建一個隊列q1,並往q1隊列中添加aaa函數。

$.queue(document,"q1",bbb);  //在document下取隊列q1,並且往q1隊列中添加bbb函數。

$.queue(document,"q1",[aaa,bbb]);  //跟上面兩行代碼的效果一樣,一次性添加多個函數

console.log($.queue(document,"q1"))   //打印出,document下的隊列q1的值,結果是:[aaa(),bbb()]

隊列中存儲的必須是函數。

$.dequeue(document,"q1")    //取出隊列中的第一項,這里是aaa,然后執行aaa函數,彈出1.

$.dequeue(document,"q1")     //取出隊列中的第一項,因為aaa已經出隊了,所以這時的第一項是bbb,因此執行bbb函數,彈出2。

以上調用的是靜態方法,那么實例方法如何調用呢?

$(document).queue("q1",aaa);   //入隊

$(document).queue("q1",bbb);  //入隊

$(document).dequeue("q1");     //出隊,執行aaa函數。

$(document).dequeue("q1");    //出隊執行bbb函數

隊列在前端開發中有什么作用呢?請看下面這個例子:

div1的寬度和高度是100,left是0.

$("#div1").click(function(){

  $(this).animate({width:300},2000);   //通過setInterval來實現,在兩秒中,把div1的寬度從100變成300

  $(this).animate({height:300},2000);   //以此類推

  $(this).animate({left:300},2000);    //以此類推

})

當點擊div1這個元素時,這個元素會先把寬度變成300px(時間2秒),然后再把高度變成300px(時間2秒),最后把left變成300px(時間2秒)。大家知道為什么會這樣了,因為animate方法其實就是一個入隊和出隊操作,入隊操作:先把寬度從100到300的函數入隊,接着把高度從100到300的函數入隊,最后把left從0到300的函數入隊。出隊操作:先執行隊列中的第一個函數,等第一函數執行結束后(把寬度從100變成300),再出隊執行第二個函數(把高度從100到300)。以此類推。

隊列queue跟Deferred延遲對象有什么區別?Deferred針對一個異步或者簡單的異步進行操作,而queue可以針對多個異步進行操作,就比如上面這個例子,隊列里面的每一項就是一個異步操作。這種多異步的操作,用Deferred實現很復雜,不可用。當然隊列也可以處理同步操作。

另外再舉一個例子:

$("#div1").click(function(){

  $(this).animate({width:300},2000).animate({left:300},2000);    

})

鏈式操作,也是先把寬度從100變成300之后,再把left從0變成300.

但是$(this).animate({width:300},2000).queue("fx",function(){}).animate({left:300},2000);    

只會把寬度從100變成300,然后就不動了。為什么呢?因為queue方法只是入隊沒有出隊,當往"fx"隊列中添加了一個空函數時,由於"fx"是animate隊列的默認值,所以這時的fx隊列有三個函數,當第一個函數執行完之后執行出隊操作,這時這個空函數執行,執行完后,沒有進行出隊操作,所以它后面的函數(把left從0變成300)不會出隊執行。

因此我們可以在空函數中進行出隊操作$(this).animate({width:300},2000).queue("fx",function(){$(this).dequeue("fx")}).animate({left:300},2000); 這時,把left從0變成300的函數就可以執行了。由於fx是隊列中的默認名字,因此上面的代碼,可以不用寫fx,也就是可以這樣寫:$(this).animate({width:300},2000).queue(function(){$(this).dequeue()}).animate({left:300},2000);當然也可以這樣寫:$(this).animate({width:300},2000).queue(function(){next()}).animate({left:300},2000);next方法也是出隊的意思。

animate方法還有第三個參數,就是回調方法,比如:

$(this).animate({width:300},2000,function(){}).animate({left:300},2000);當把div的寬度從100變成300時(2秒),就會執行function(){},在執行function里面的代碼后,就會立即執行把left從0到300的函數。如果function(){}里面有一個定時器,比如:把高度從100變成300的定時器函數。這時就會出現高度和left同時變化的情況。

由此可知animate的回調方法,不可控,意思就是它的回調方法執行后會立即執行后面的animate中添加的定時器函數。但是queue就不一樣,它是可控的,我們可以等function函數執行完成后(也就是說把function里面的把高度從100變成300的定時器函數執行結束后),再調用next方法,這時它后面的animate添加的定時器函數才會執行,也就是進行出隊,執行把left從0變成300的定時器函數。能很方便的控制異步操作的流程。

接下來我們來看源碼實現:

jQuery.extend({
  queue: function( elem, type, data ) {
    var queue;

    if ( elem ) {    //當有元素時,才進行操作。比如:$.queue(document,"q1",aaa);document就是elem
      type = ( type || "fx" ) + "queue";   //如果沒有傳入q1那么type默認為fx,加上queue字符串
      queue = data_priv.get( elem, type ); //去數據緩存中獲取此元素的q1queue屬性值,第一次時,是undefined。

      if ( data ) {     //data就是aaa
        if ( !queue || jQuery.isArray( data ) ) {   //如果取的值不存在,第一次時是不存在的。進入if語句
          queue = data_priv.access( elem, type, jQuery.makeArray(data) );//把data也就是aaa函數轉換成數組形式,也就是變成[aaa()]
        } else {       //當第二次執行時,也就是$.queue(document,"q1",bbb); 因為里面已經有[aaa()]了,因此直接把data(這里是bbb函數)push到q1queue屬性值中。當然這里有一個例外,比如:第二次執行時,是這樣的$.queue(document,"q1",[bbb]);傳入的data是個數組,這時不會走這里,而是走if語句,這樣的話,會把之前的[aaa()]覆蓋掉,只會有[bbb()]。

          queue.push( data );
        }
      }
      return queue || [];   //返回這個隊列,其實就是這個數組[aaa(),bbb()]
    }
  },

  dequeue: function( elem, type ) {  //$.dequeue(document,"q1") 
    type = type || "fx";   

    var queue = jQuery.queue( elem, type ),  //先獲取這個q1隊列的值
      startLength = queue.length,
        fn = queue.shift(),    //取隊列中的第一項
          hooks = jQuery._queueHooks( elem, type ),  //hooks其實是元素elem在數據緩存中的一個屬性對象,如果我們調用的是$.dequeue(document,"q1") 的話,那么屬性對象名就是q1queueHooks,屬性值是{empty: jQuery.Callbacks("once memory").add(function() { data_priv.remove( elem, [ type + "queue", key ] );})}。因此你使用hooks.empty,其實就是q1queueHooks.empty。
        
            next = function() {  //這個next方法其實就是出隊
              jQuery.dequeue( elem, type );
            };

    if ( fn === "inprogress" ) {  //這里為什么會出現inprogress呢?舉個例子:$(this).animate({width:300},2000,function(){}).animate({left:300},2000);這個代碼的意思是入隊兩個定時器函數,第一個定時器函數是把寬度從100變成300,第二個定時器函數是把left從0變成300.如果這里只有入隊操作,沒有出隊操作,那么這兩個定時器函數都不會執行,因此大家可以去看queue的實例方法,源碼在下面,里面有這樣一個判斷:if ( type === "fx" && queue[0] !== "inprogress" ) {jQuery.dequeue( this, type );},animate的入隊,默認隊列為fx,而且它的隊列的第一項不是inprogress,而是第一個定時器函數,這時進入if語句,進行出隊。因此才能執行第一個定時器函數。那么第二個定時器函數來入隊時,也會馬上出隊嗎?不會,不然的話,兩個定時器函數會同時執行了。那么第二個定時器函數為什么沒有立馬出隊,是因為第一個定時器函數出隊時,會在fx隊列前面添加inprogress,因此第二個定時器函數入隊時,fx隊列的第一個項就是inprogress,因而不會進行出隊操作。 第一個定時器函數執行完之后,就會進行再次出隊,這時第二個定時器函數就會執行了。
        
      fn = queue.shift();   //如果取出的隊列的第一項是inprogress,這時隊列是[bbb()],因為inprogress已經出隊了,就再次出隊,這時bbb()出隊,隊列為[],fn為bbb。
      startLength--;  //startLength變成1
    }

    if ( fn ) {    //當數組為["inprogress"]出隊時,fn = undefined,startLength=0;這時就會結束隊列操作了。

      if ( type === "fx" ) {   //當是默認隊列時,也就是animate操作時,就會先往隊列的前面添加inprogress
        queue.unshift( "inprogress" );   //隊列變成 ["inprogress"],這時就會執行bbb(),執行完之后,又出隊。
      }

      delete hooks.stop;
      fn.call( elem, next, hooks );   //這里就會執行第一個定時器函數,執行完之后,就會調用next方法,進行出隊。這時的隊列是["inprogress",bbb()]

    }

    if ( !startLength && hooks ) {  //當隊列結束后,清理數據緩存中隊列數據
      hooks.empty.fire();  //這里執行fire方法,就會觸發add添加的方法,也就是data_priv.remove( elem, [ type + "queue", key ] );把緩存數據中的所有隊列信息,以及q1queueHooks一起刪除掉。
    }
  },

  _queueHooks: function( elem, type ) {
    var key = type + "queueHooks";
    return data_priv.get( elem, key ) || data_priv.access( elem, key, {
      empty: jQuery.Callbacks("once memory").add(function() {  //empty的屬性值是一個Callbacks對象,Callbacks的特點是可以通過它的add方法添加函數,當調用Callbacks的fire方法時,就會執行add添加的方法。
        data_priv.remove( elem, [ type + "queue", key ] );
      })
    });
  }
});

jQuery.fn.extend({
  queue: function( type, data ) {  // $(document).queue("q1",aaa);
    var setter = 2;

    if ( typeof type !== "string" ) {  //當type不等於字符串時,也就是這種情況時:$(document).queue(aaa);
      data = type;   //data = aaa,type = "fx",setter =1
      type = "fx";
      setter--;
    }

    if ( arguments.length < setter ) {   //這里判斷是獲取,還是設置。比如:$(document).queue("q1"),這里是獲取操作,因此進入if語句。
      return jQuery.queue( this[0], type );   //獲取是針對一組元素的第一個元素。
    }

    return data === undefined ? this : this.each(function() {  //這里就是設置操作,對每個元素都進行設置
      var queue = jQuery.queue( this, type, data );  //入隊操作,會在緩存系統中添加一個隊列q1,隊列中,入隊aaa。

      jQuery._queueHooks( this, type );    //設置元素的hooks對象,會在緩存系統中添加一個hooks屬性,它可以移除緩存系統中與元素this,相關的隊列操作的所有數據。

      if ( type === "fx" && queue[0] !== "inprogress" ) {  //跟靜態方法的queue的思路一樣。
        jQuery.dequeue( this, type );
      }
    });
  },
  dequeue: function( type ) {   //$(document).dequeue("q1"); 出隊操作,是針對一組元素的。也就是說如果有多個document被匹配上,那么會對每個document都做出隊操作。
    return this.each(function() {
      jQuery.dequeue( this, type );
    });
  },
  delay: function( time, type ) {  //第一個參數是延遲的時間,第二個參數是哪個隊列(隊列的名字)延遲,我們先來舉個例子說下delay方法的作用:$(this).animate({width:300},2000).delay(2000).animate({left:300},2000);這個代碼的意思是:第一個定時器函數執行結束后,會延遲兩秒鍾,才會執行第二個定時器函數。
    time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;   //jQuery.fx.speeds = {slow: 600,fast: 200,_default: 400};意思就是說,你delay里面是否寫了"slow","fast",或"_default"。如果是,就直接調用默認的值,如果傳入的是數字,那么就只用數字。

    type = type || "fx";

    return this.queue( type, function( next, hooks ) {
      var timeout = setTimeout( next, time );   //延遲time秒,再進行出隊。意思就是time秒后,第二個定時器函數才會執行。
      hooks.stop = function() {   //這個方法會清除定時器,如果執行,next方法就不會執行,也就不會出隊了
        clearTimeout( timeout );
      };
    });
  },
  clearQueue: function( type ) {
    return this.queue( type || "fx", [] );   //把隊列變成空數組,上面說到,如果傳入數組,會覆蓋隊列的原數組
  },
  promise: function( type, obj ) {  //type是指隊列的名字,如果此type的隊列全部出隊后,就會執行done添加的方法。我們先舉個例子說下這個方法的作用:$(this).animate({width:300},2000).animate({left:300},2000);$(this).promise().done(function(){alert(3)});這句代碼的意思是,等上面兩個定時器函數都執行結束后(因為他們默認處理的都是fx隊列)。才會執行彈出3的函數。
    var tmp,
      count = 1,
        defer = jQuery.Deferred(),   //新建一個延遲對象
          elements = this,
            i = this.length,   //元素的個數,這里假設是一個document元素
              resolve = function() {
                if ( !( --count ) ) {
                  defer.resolveWith( elements, [ elements ] );
                }
              };

    if ( typeof type !== "string" ) {    //如果沒傳入隊列名,就用fx默認隊列
      obj = type;
      type = undefined;
    }
    type = type || "fx";

    while( i-- ) {    //執行一次
      tmp = data_priv.get( elements[ i ], type + "queueHooks" );  //去緩存系統找跟這個元素有關的數據
      if ( tmp && tmp.empty ) {   //如果存在,就證明隊列中有定時器函數要執行。進入if語句
        count++;   //count等於2
        tmp.empty.add( resolve );   //當調用tmp.empty.fire方法時,就會執行resolve 方法。而這里會等fx類型的隊列全部出隊后(這兩個定時器函數都執行結束后),才會觸發fire方法,這時就會執行add添加的所有方法,resolve就是其中一個,於是count就會變成0(在出隊列時,下面的resolve方法已經執行一次了),進入if語句,執行延遲對象的resolveWith,而此方法,就會觸發延遲對象的done方法添加的函數,因此彈出3的函數執行。
      }
    }
    resolve();   //這里會先執行一次resolve方法,count--,變成1。
    return defer.promise( obj );  //返回這個延遲對象。
  }
});

 這一課的知識點牽涉到前面的Callbacks,Deferred等方法,因此沒看懂這兩個東西,這個queue當然有看不懂。因此騷年們,加油吧!

 

 

 

加油!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM