[原创] jQuery源码分析-08队列 Queue
作者:nuysoft/高云 QQ:47214707 EMail:nuysoft@gmail.com
声明:本文为原创文章,如需转载,请注明来源并保留原文链接。
读读写写,不对的地方请告诉我,多多交流共同进步,本章的的PDF下载在最后。
前记:
国庆给自己放了个安静的长假,日游杭州大小景点,夜宿西湖边上,于大街小巷中遍尝美味小吃,没有电脑没有网络,这样的日子真是是好日子啊;回京开始工作了,编程是我的兴趣,虽然变成了工作,但是享受的心态要继续保持下去。
白天工作,不管忙不忙,jQuery源码分析系列只能放在晚上写,经常看的朋友兴许也注意到更新时间一般是凌晨,经常觉的挺累的,想今天算了吧上床睡觉明天再说吧,但还是坚持下来了,我会尽量要求自己1-2天发布一篇。刚开始写的时候,有些担心写出来的东西会幼稚肤浅或讲不清楚,有些地方也确实是这样,这个假期让我想明白了很多事,以后的文章欢迎各位道友拍各种砖和石头,用力点,不要停。
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); } ) |
2. 查看jQuery.data为body分配的唯一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,存储为内部数据(pvt为true) 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 ), // 取出队列,调用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, 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 实现思路
上一篇: 幽默青年糗事,可劲的笑吧
下一篇: Vue指令的钩子函数使用方法
推荐阅读
-
jQuery 源码分析(十一) 队列模块 Queue详解
-
jQuery源码分析之异步队列 Deferred 使用介绍
-
jQuery源码分析(九) 异步队列模块 Deferred 详解
-
jQuery源码分析-05异步队列 Deferred 使用介绍_jquery
-
jQuery源码分析-05异步队列 Deferred 使用介绍_jquery
-
jQuery 源码分析笔记(7) Queue_jquery
-
jQuery 源码分析笔记(7) Queue_jquery
-
jQuery源码分析之异步队列 Deferred 使用介绍
-
[原创] jQuery源码分析-03构造jQuery对象-源码结构和核心函数
-
[原创] jQuery源码分析-09属性操作