欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[原创] jQuery源码分析-08队列 Queue

程序员文章站 2022-04-26 17:37:04
...

作者:nuysoft/高云 QQ:47214707 EMail:nuysoft@gmail.com

声明:本文为原创文章,如需转载,请注明来源并保留原文链接。 

 

读读写写,不对的地方请告诉我,多多交流共同进步,本章的的PDF下载在最后。

 

前记:

国庆给自己放了个安静的长假,日游杭州大小景点,夜宿西湖边上,于大街小巷中遍尝美味小吃,没有电脑没有网络,这样的日子真是是好日子啊;回京开始工作了,编程是我的兴趣,虽然变成了工作,但是享受的心态要继续保持下去。

白天工作,不管忙不忙,jQuery源码分析系列只能放在晚上写,经常看的朋友兴许也注意到更新时间一般是凌晨,经常觉的挺累的,想今天算了吧上床睡觉明天再说吧,但还是坚持下来了,我会尽量要求自己1-2天发布一篇。刚开始写的时候,有些担心写出来的东西会幼稚肤浅或讲不清楚,有些地方也确实是这样,这个假期让我想明白了很多事,以后的文章欢迎各位道友拍各种砖和石头,用力点,不要停。

 

8. 队列 Queue

8.1        概述

队列是一种特殊的线性表,只允许在表的前端(队头)进行删除操作(出队),在表的后端(队尾)进行插入操作(入队)。队列的特点是先进先出(FIFO-first in first out),即最先插入的元素最先被删除。

jQuery提供了jQuery.queue/dequeuejQuery.fn.queue/dequeue,实现对队列的入队、出队操作。不同于队列定义的是,jQuery.queuejQuery.fn.queue不仅执行出队操作,返回队头元素,还会自动执行返回的队头元素。

8.2        用途

jQuery源码中,仅用于动画模块,这里入队的函数的功能是:

l  遍历要动画的属性,修正要执行的动画动作,修正/备份属性

l  遍历要动画的属性,为每一个属性创建jQuery.fx对象,计算起始值和结束值,调用fx对象的custom开始动画

看看源码:

/**

 * .animate( properties, [duration], [easing], [complete] )

 *

 * .animate( properties, options )

 *

 * animate做了三件事:

 * 1. 调用jQuery.speed修正传入的参数(时间、算法、回调函数)

 * 2. 遍历要动画的属性,修正要执行的动画动作,修正/备份属性

 * 3. 遍历要动画的属性,为每一个属性创建jQuery.fx对象,计算起始值和结束值,调用fx对象的custom开始动画

 */

animate: function( prop, speed, easing, callback ) {

    var optall = jQuery.speed(speed, easing, callback); // 修正参数

 

    // 如果是空对象,则直接运行callback

    if ( jQuery.isEmptyObject( prop ) ) {

       return this.each( optall.complete, [ false ] );

    }

 

    // Do not change referenced properties as per-property easing will be lost

    // 复制一份prop,不改变原有的属性

    prop = jQuery.extend( {}, prop );

    // queuefalse,动画立即开始,否则则放入动画队列

    return this[ optall.queue === false ? "each" : "queue" ](function() {

       // XXX 'this' does not always have a nodeName when running the

       // test suite

 

       if ( optall.queue === false ) {

           jQuery._mark( this );

       }

 

       var opt = jQuery.extend( {}, optall ), // 复制一份

           isElement = this.nodeType === 1,

           hidden = isElement && jQuery(this).is(":hidden"), // 是否隐藏

           name, val, p,

           display, e,

           parts, start, end, unit;

 

       // will store per property easing and be used to determine when an animation is complete

       // 已完成的属性

       opt.animatedProperties = {};

      

       // 遍历每一个属性,本次遍历仅仅是修正和记录属性值,为后边的动画做准备

       for ( p in prop ) {

           // property name normalization

           // property格式化,转换为驼峰式,因为style的属性名是驼峰式

           name = jQuery.camelCase( p );

           if ( p !== name ) {

              prop[ name ] = prop[ p ];

              delete prop[ p ];

           }

 

           val = prop[ name ];

 

           // easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default)

           // 样式值允许以数组的方式,数组第一个表示动画是否完成,第二个表示动画的值,这样做有两点好处:

           // 1. 允许在某些属性还未完成时就执行回调函数、执行下一个动画

           // 2. 允许为默写属性指定算法

           if ( jQuery.isArray( val ) ) {

              opt.animatedProperties[ name ] = val[ 1 ]; //

              val = prop[ name ] = val[ 0 ];

           } else {

              // 对指定属性设置动画算法

              opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing';

           }

          

           // 如果属性值是hide,并且已经隐藏,则直接调用回调函数;show同理

           if ( val === "hide" && hidden || val === "show" && !hidden ) {

              return opt.complete.call( this );

           }

 

           // 如果是width/height,修正它的:

           // overflow

           // display

           if ( isElement && ( name === "height" || name === "width" ) ) {

              // Make sure that nothing sneaks out

              // Record all 3 overflow attributes because IE does not

              // change the overflow attribute when overflowX and

              // overflowY are set to the same value

              opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ];

 

              // Set display property to inline-block for height/width

              // animations on inline elements that are having width/height

              // animated

              if ( jQuery.css( this, "display" ) === "inline" &&

                     jQuery.css( this, "float" ) === "none" ) {

                  if ( !jQuery.support.inlineBlockNeedsLayout ) {

                     this.style.display = "inline-block";

 

                  } else {

                     display = defaultDisplay( this.nodeName );

 

                     // inline-level elements accept inline-block;

                     // block-level elements need to be inline with layout

                     if ( display === "inline" ) {

                         this.style.display = "inline-block";

 

                     } else {

                         this.style.display = "inline";

                         this.style.zoom = 1;

                     }

                  }

              }

           }

       }

      

       // 如果是width/height,先设置为超出部分隐藏

       if ( opt.overflow != null ) {

           this.style.overflow = "hidden";

       }

      

       // 遍历每一个属性,

       for ( p in prop ) {

           e = new jQuery.fx( this, opt, p ); // 构造动画fx对象

           val = prop[ p ];

 

           // 如果是toggle/show/hide

           if ( rfxtypes.test(val) ) {

              // 如果是toggle,则判断当前是否hidden,如果hiddenshow,否则hide

              // 如果不是toggle,说明valhide/show之一

              e[ val === "toggle" ? hidden ? "show" : "hide" : val ]();

 

           } else {

              parts = rfxnum.exec( val ); // 数值型,1+=/-=2数值,3单位

              start = e.cur(); // 取出当前值

 

              // 如果是数值型

              if ( parts ) {

                  end = parseFloat( parts[2] ); // 包括了正负号

                  unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" ); // jQuery.cssNumber中包含的是无单位的数值型属性,比如zIndex/zoom

 

                  // We need to compute starting value

                  // 计算开始值

                  if ( unit !== "px" ) {

                     jQuery.style( this, p, (end || 1) + unit);

                     start = ((end || 1) / e.cur()) * start;

                     jQuery.style( this, p, start + unit);

                  }

 

                  // If a +=/-= token was provided, we're doing a relative animation

                  // 如果以+=/-=开头,则做相对动画

                  if ( parts[1] ) {

                     // 计算出相对值

                     end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start;

                  }

                  // 开始执行动画

                  e.custom( start, end, unit );

              // 不是数值型

              } else {

                  // 开始执行动画

                  e.custom( start, val, "" );

              }

           }

       }

 

       // For JS strict compliance

       // 严格遵守?

       return true;

    });

}

 

8.3        实现思路

jQuery队列的实现依赖于jQuery.data,用数组实现,作为私有数据存储在jQuery的全局变量jQuery.cache中。

l  调用jQuery.queue入队时,如果不传入队列名,则默认为fx(标准动画)

    n  队列用数组实现,入队直接调用数组对象的方法push

    n  入队的元素必须是函数,或由函数构成的数组

    n  所有队列名会自动加上queue后缀,表示这是一个队列

    n  如果传入的是数组,则覆盖现有的队列

    n  如果不是数组,则直接入队

l  调用jQuery. dequeue出队时,会先调用jQuery.queue取得整个队列,因为队列用数组实现,可以调用数组的shift方法取出第一个元素并执行

    n  执行第一个元素时采用function.call( context, args ),由此可以看出jQuery队列只支持函数(这么说不完全准确,fx动画是个特例,会在队列头端插入哨兵inprogress,类型为字符串)

    n  出队的元素会自动执行,无论这个元素是不是函数,如果不是函数此时就会抛出异常(这个异常并没有处理)

    n  如果队列变成空队列,则用关键delete删除jQuery.cachetype对应的属性

8.4        验证(firefox+firebug

我们验证一下上面的思路:

1.     先入队3个弹窗函数,分别弹出123

$('body').queue( 'test', function(){ alert(1); } )

$('body').queue( 'test', function(){ alert(2); } )

$('body').queue( 'test', function(){ alert(3); } )

2.     查看jQuery.databody分配的唯一id(为什么要查看body的唯一id,请参考数据缓存的解析)

>>> $.expando

"jQuery161017518149125935123"

command: >>> $('body')[0][$.expando]

5

>>> $('body')[0]["jQuery161017518149125935123"]

5

$.expando有三部分构成:字符串"jQuery" + 版本号jQuery.fn.jquery + 随机数Math.random(),因此每次加载页面后都不相同。

3.     查看jQuery.cache对属性5对应的数据,格式化如下:

{

  "1" : { ... },

  "2" : { ... },

  "3" : { ... },

  "4" : { ... },

"5" : {

"jQuery161017518149125935123" : {

  "testqueue" : [

    (function () {alert(1);}),

    (function () {alert(2);}),

    (function () {alert(3);})

]

}

}   

}

内部数据存储在$.expando属性("jQuery161017518149125935123"中,这点区别于普通数据

4.     外事具备,我们出队试试,连续3次调用出队$('body').dequeue( 'test' ),每次调用dequeue后用$('body').queue('test').length检查队列长度

控制台命令

$('body').dequeue( 'test' )

$('body').dequeue( 'test' )

$('body').dequeue( 'test' )

浏览器截图

见附件PDF 见附件PDF 见附件PDF

队列长度

2

1

0

果不其然,调用出队函数dequeue后,入队的函数按照先进先出的顺序,依次被执行

5.     最后看看全部出队后,jQuery.cache中的状态

>>> $.cache[5][$.expando]['testqueue']

undefined

可以看到,testqueue属性已经从body的缓存中移除

8.5        源码分析

jQuery.extend({

    // 计数器,用在动画animate

    _mark: function( elem, type ) {

       if ( elem ) {

           type = (type || "fx") + "mark";

           // 取出数据加1,存储在内部对象上

           jQuery.data( elem, type, (jQuery.data(elem,type,undefined,true) || 0) + 1, true );

       }

    },

    // 用在动画animate

    _unmark: function( force, elem, type ) {

       if ( force !== true ) {

           type = elem;

           elem = force;

           force = false;

       }

       if ( elem ) {

           type = type || "fx";

           var key = type + "mark",

              // 1

              count = force ? 0 : ( (jQuery.data( elem, key, undefined, true) || 1 ) - 1 );

           if ( count ) {

              jQuery.data( elem, key, count, true );

           } else {

              jQuery.removeData( elem, key, true );

              handleQueueMarkDefer( elem, type, "mark" );

           }

       }

    },

    // jQuery.queue( element, [queueName] )

    // 返回在指定的元素element上将要执行的函数队列

   

    // jQuery.queue( element, queueName, newQueue or callback )

    // 修改在指定的元素element上将要执行的函数队列

    // 使用jQuery.queue添加函数后,最后要调用jQuery.dequeue(),使得下一个函数能线性执行

    //

    // 调用jQuery.data,存储为内部数据(pvttrue

    queue: function( elem, type, data ) {

       // elem必须存在,否则没有意义

       if ( elem ) {

           type = (type || "fx") + "queue"; // 改名,每个都要加上queue

           // 取出队列

           var q = jQuery.data( elem, type, undefined, true );

           // Speed up dequeue by getting out quickly if this is just a lookup

           // 如果data存在,才会进行后边转换数组、入队等操作,可以加速取出整个队列

           if ( data ) {

              // 如果队列不存在,或者data是数组,则调用makeArray转换为数组,并覆盖队列(入队)

              if ( !q || jQuery.isArray(data) ) {

                  // 用数组实现队列

                  q = jQuery.data( elem, type, jQuery.makeArray(data), true );

              } else {

                  // 队列存在的话,且data不是数组,直接入队

                  // 这里并没有判断data的类型,不管data是不是函数

                  q.push( data );

              }

           }

           // 返回队列(即入队的同时,返回整个队列)

           // 简洁实用的避免空引用的技巧

           return q || [];

       }

    },

    // 出队并执行

    // 调用jQuery.queue取得整个队列,在调用shift取出第一个元素

    dequeue: function( elem, type ) {

       type = type || "fx"; // 默认fx,但是入队时不是被改为fxqueue了么,别着急!

 

       var queue = jQuery.queue( elem, type ), // 取出队列,调用queuetype变成了type+quque

           fn = queue.shift(), // 取出第一个

           defer;

 

       // If the fx queue is dequeued, always remove the progress sentinel

       // 如果取出的fn是一个正在执行中标准动画fx,抛弃执行哨兵(inprogress),再取一个

       if ( fn === "inprogress" ) {

           fn = queue.shift();

       }

 

       if ( fn ) {

           // Add a progress sentinel to prevent the fx queue from being

           // automatically dequeued

           // 如果是标准动画,则在队列头部增加处理中哨兵属性,阻止fx自动处理

           if ( type === "fx" ) {

              // 在队列头部增加哨兵inprogress

              queue.unshift("inprogress");

           }

           // 执行取出的fn,并传入回调函数jQuery.dequeue

           // 可以看到fn必须是函数,否则会出错

           fn.call(elem, function() {

              // 但是这个回调函数不会自动执行

              jQuery.dequeue(elem, type);

           });

       }

       // 如果执行完毕,则调用jQuery.removeData移除type指定的队列

       // 此时的队列成为空队列,实质是一个空数组,jQuery.removeData内部使用delete关键字删除type对应的空数组

       if ( !queue.length ) {

           jQuery.removeData( elem, type + "queue", true );

           handleQueueMarkDefer( elem, type, "queue" );

       }

    }

});

 

jQuery.fn.extend({

    // queue( [ queueName ] )

    // 返回在指定的元素element上将要执行的函数队列

    //

    // queue( [ queueName ], newQueue or callback )

    // 修改在指定的元素element上将要执行的函数队列

    queue: function( type, data ) {

       // 修正参数:只传了一个非字符串的参数,则默认为动画fx

       if ( typeof type !== "string" ) {

           data = type;

           type = "fx";

       }

       // 如果data等于undefined,则认为是取队列

       if ( data === undefined ) {

           return jQuery.queue( this[0], type );

       }

       // 如果传入了data参数,则在每一个匹配的元素上执行入队操作

       // jQuery中,使用each遍历匹配的元素,是一种安全的惯例做法

       return this.each(function() {

           var queue = jQuery.queue( this, type, data );

           // 如果动画执行完毕(即不是inprogress),则从队列头部移除

           if ( type === "fx" && queue[0] !== "inprogress" ) {

              jQuery.dequeue( this, type );

           }

       });

    },

    // 调用jQuery.dequeue出队

    dequeue: function( type ) {

       return this.each(function() {

           jQuery.dequeue( this, type );

       });

    },

    // Based off of the plugin by Clint Helfers, with permission.

    // http://blindsignals.com/index.php/2009/07/jquery-delay/

    // 延迟执行队列中未执行的函数,通过在队列中插入一个延时 出队的函数来实现

    delay: function( time, type ) {

       time = jQuery.fx ? jQuery.fx.speeds[time] || time : time;

       type = type || "fx";

 

       return this.queue( type, function() {

           var elem = this;

           setTimeout(function() {

              jQuery.dequeue( elem, type );

           }, time );

       });

    },

    // 清空队列,通过将第二参数设置为空数组[]

    clearQueue: function( type ) {

       return this.queue( type || "fx", [] );

    },

    // Get a promise resolved when queues of a certain type

    // are emptied (fx is the type by default)

    // 返回一个只读视图,当队列中指定类型的函数执行完毕后

    promise: function( type, object ) {

       if ( typeof type !== "string" ) {

           object = type;

           type = undefined;

       }

       type = type || "fx";

       var defer = jQuery.Deferred(),

           elements = this,

           i = elements.length,

           count = 1,

           deferDataKey = type + "defer",

           queueDataKey = type + "queue",

           markDataKey = type + "mark",

           tmp;

       function resolve() {

           if ( !( --count ) ) {

              defer.resolveWith( elements, [ elements ] );

           }

       }

       while( i-- ) {

           if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) ||

                  ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) ||

                     jQuery.data( elements[ i ], markDataKey, undefined, true ) ) &&

                  jQuery.data( elements[ i ], deferDataKey, jQuery._Deferred(), true ) )) {

              count++;

              tmp.done( resolve );

           }

       }

       resolve();

       return defer.promise();

    }

});

8.6        总结

jQuery队列的实现并不复杂,它的核心思路是:

在传统队列的实现上增加了出队自动执行,执行完成后再次自动出队。

详见小节:8.3 实现思路