8. 队列 Queue

8.1        概述

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

jQuery提供了jQuery.queue/dequeue和jQuery.fn.queue/dequeue,实现对队列的入队、出队操作。不同于队列定义的是,jQuery.queue和jQuery.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 );

// queue为false,动画立即开始,否则则放入动画队列

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,如果hidden则show,否则hide

// 如果不是toggle,说明val是hide/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.cache中type对应的属性

8.4        验证(firefox+firebug)

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

  1. 先入队3个弹窗函数,分别弹出1、2、3
$(‘body’).queue( ‘test’, function(){ alert(1); } )

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

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

  1. 查看jQuery.data为body分配的唯一id(为什么要查看body的唯一id,请参考数据缓存的解析)
>>> $.expando

“jQuery161017518149125935123”

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

5

>>> $(‘body’)[0][“jQuery161017518149125935123”]

5

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

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

“1” : { … },

“2” : { … },

“3” : { … },

“4” : { … },

“5” : {

“jQuery161017518149125935123” : {

“testqueue” : [

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

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

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

]

}

}

}

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

  1. 外事具备,我们出队试试,连续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后,入队的函数按照先进先出的顺序,依次被执行

  1. 最后看看全部出队后,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, undefinedtrue) || 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,存储为内部数据(pvt为true)

queue: function( elem, type, data ) {

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

if ( elem ) {

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

// 取出队列

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

// 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 ), // 取出队列,调用queue时type变成了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, undefinedtrue ) ||

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

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

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

count++;

tmp.done( resolve );

}

}

resolve();

return defer.promise();

}

});

8.6        总结

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

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

详见小节:8.3 实现思路

发表评论

电子邮件地址不会被公开。 必填项已用*标注