jQuery使用(九):队列及实现原理、基于队列模拟实现animate()
- 开篇一张图之队列模型
- queue()如何使用?
- queue()原理实现?
- 基于queue()模拟实现animate()
一、使用queuer方法、理解队列原理
- queue()
- dequeue()
- clearQueue()
1.创建队列$(selector).queue(queueName,function);
//html --css省略 <div class="demo"></div> //js -- 创建队列chain,并传入3个方法 $(".demo").queue("chain",function(){ console.log("over1"); }).queue("chain",function(){ console.log("over2"); }).queue("chain",function(){ console.log("over3"); });
2.查看队列:
console.log( $(".demo").queue("chain") ); //打印结果:[function, function, function]
3.出队:$(selector).dequeue(queueName)
$(".demo").dequeue("chain");//打印:over1 console.log( $(".demo").queue("chain") );//打印:[function, function] $(".demo").dequeue("chain");//打印:over2 console.log( $(".demo").queue("chain") );//打印:[function]
在第一次出队操作后,查看控制台打印的队列数组索引任然是从0开始,说明队列出队操作是执行最先添加的函数,并在执行后删除这个函数。而且依然保持数组索引从零开始。
4.从前面的出队机制来看有点鸡肋,每次都是触发一个函数,删除一个函数,如果有需求是要一次完全触发执行呢?至少在animate的动画实现来看就是一次触发全部执行。所以jQuery提供了这样的机制,就是我们在创建队列传入函数的时候,给函数传入参数next,并在函数内最末尾处添加代码next();这样就可以实现触发一次可以执行最开始的函数,并且可以接着执行下一个函数,而且这两个函数都会被出队。
$(".demo").queue("chain",function(next){ console.log("over1"); next(); }).queue("chain",function(next){ console.log("over2"); next(); }).queue("chain",function(){ console.log("over3"); }); $(".demo").dequeue("chain");//打印:over1 over2 over3 console.log( $(".demo").queue("chain") );//打印:[]
5.清空队列$(selector).clearQueue(queueName):clearQueue() 方法从尚未运行的队列中移除所有项目。
$(".demo").clearQueue("chain"); console.log( $(".demo").queue("chain") );//[]
二、jQuery之animate中的queue(队列)(实现原理)
队列作为一种数据结构其底层实质就是数据,因为数组本身就是有序列的数据结构,只是队列在数组的基础上规定了添加、调用、删除三个固定的行为,为什么叫做固定的行为呢?就是因为给队列添加数据单元的时候只能添加在末尾;而队列执行就是调用加删除,而且只能从开始处调用一个,调用同时从队列中删除该数据单元。所以,这里需要用到数据的两个原生操作方法push()和shift():在数组末尾添加和删除数组第一个元素并返回该元素。下面是在仿写jQuery的jQuery对象下封装的queue方法源码:
//队列(入队) -- 添加队列 - 往已有的队列添加内容 jQuery.prototype.myQueue = function(){ var queueName = arguments[0] || 'fx'; var addFunc = arguments[1] || null; var len = arguments.length; //获取队列 if(len == 1){ return this[0].queueObj[queueName]; } //queue Data dom {chain : []} -- 添加队列 || 往已有队列中添加内容 for(var i = 0; i < this.length; i ++){ if(this[i].queueObj == undefined){ this[i].queueObj = {} } this[i].queueObj[queueName] == undefined ? this[i].queueObj[queueName] = [addFunc] : this[i].queueObj[queueName].push(addFunc); } return this; }
在jQuery源码中,队列数据是通过jQuery的data机制存储了,由于data机制比较复杂,这里就在DOM原型上添加了一个queueObj属性来存储队列数据。jQuery源码中queue方法实现了获取队列和相队列添加数据的功能,所以这两个功能也一并在myQueue方法中实现了,并且也可以实现DOM集合的jQuery对象的逐个操作,这个和元素方法使用已经没有差别,只有数据存储的差异了。
接着我们再来看看dequeue出队的方法实现源码:
////队列 -- 出队 jQuery.prototype.myDequeue = function(type){ var queueName = arguments[0] || 'fx'; var queueArr = []; var currFunc = null; var next = null; for(var i = 0; i < this.length; i ++){ var self = this[i]; queueArr = jQuery(self).myQueue(queueName); currFunc = queueArr.shift(); if(currFunc == undefined){ break; } next = function(){ jQuery(self).myDequeue(queueName); } currFunc(next); } return this; }
出队时需要注意一点就是给animate()动画方法设置默认队列名称“fx”,同时也具备全部jQuery对象的所有DOM执行出队操作。
一、基于queuer方法实现原生jQuery动画函数animate()
其实大部分的代码已经在定时点的运动中实现了,而且从实现功能来看定时定点运动函数已经可以通过回调函数来实现了,但是在jQuery源码中,animate()方法是通过队列的方式来取代回调模式,这是为了更好的面向实际开发需要,队列模式相比回调模式更容易维护,出现异常更容易排除,后期会有关于回调的探讨博客,会对这个问题做具体的讨论。
当然是用队列的方式来实现animate()还有更关键的原因,就是可以控制动画延迟,停止,取消动画效果,具体可以了解jQuery使用(八):运动方法。这里我们今天暂时实现animate()方法和动画延迟方法delay(),要实现停止和取消动画效果还需要其他功能协助才能完成,那是后面的事了。下面是animate()实现源码:
1 //动画函数 -- 模拟实现animate -- 暂时实现基于目标点和回调函数两个参数的动画 2 //参数:{styles},speed,easing,callback 3 jQuery.prototype.myAnimate = function(json,speed,easing,callback){ 4 var len = this.length; 5 var self = this; 6 //最后添加到队列里的内容函数 7 var baseFunc = function(next){ 8 var times = 0; //记录到达目标点的DOM个数,用于判断是否是否DOM都到达目标点 9 for(var i = 0; i < len; i++){ 10 startMove(self[i],json,speed,easing,function(){ 11 times++; 12 if(times == len){ //如果所有DOM动画执行完毕,调用回调函数执行 13 callback && callback(); 14 next(); //所有DOM执行完动画后,并且回调函数执行完,执行动画队列的下一个动画 15 } 16 }); 17 } 18 } 19 this.myQueue('fx',baseFunc); 20 if(this.myQueue('fx').length == 1){ 21 this.myDequeue('fx'); 22 } 23 //获取dom样式 24 function getStyle(obj,attr){ 25 if(window.getComputedStyle){ 26 return window.getComputedStyle(obj,false)[attr]; 27 }else{ 28 return obj.currentStyle[attr]; 29 } 30 } 31 32 //运动方法 -- 具体参照博客《原生JavaScript运动功能系列(五):定时定点运动》 33 function startMove(obj,json,speed,easing,callback){ 34 var initialPlace = {}; 35 var nowPlace; 36 clearInterval(obj.timer); 37 var createTime = function(){ 38 return (+new Date); 39 } 40 var startTime = createTime(); 41 for(var attr in json){ 42 if(attr == 'opacity'){ 43 initialPlace[attr] = Math.round(parseFloat(getStyle(obj,attr))*100); 44 }else{ 45 initialPlace[attr] = parseInt(getStyle(obj,attr)); 46 } 47 } 48 if(!easing){ 49 easing = jQuery.easingObj.swing; 50 }else{ 51 easing = jQuery.easingObj[easing]; 52 } 53 obj.timer = setInterval(function(){ 54 var remaining = Math.max(0, startTime + speed - createTime()); 55 var temp = remaining / speed || 0; 56 var percent = 1 - temp; 57 for(var attr in json){ 58 nowPlace = (json[attr] - initialPlace[attr]) * easing(percent) + initialPlace[attr]; 59 if(attr == 'opacity'){ 60 obj.style.opacity = nowPlace / 100; 61 }else{ 62 obj.style[attr] = nowPlace + 'px'; 63 } 64 } 65 if(percent == 1){ 66 clearInterval(obj.timer); 67 typeof callback == 'function' ? callback() : ''; 68 } 69 },30); 70 } 71 return this; 72 }
由于这部分代码量比较大,整体方法的代码我先折叠,但是别急,我把关键的代码提炼出来分析:
1 var len = this.length; 2 var self = this; 3 //最后添加到队列里的内容函数 4 var baseFunc = function(next){ 5 var times = 0; //记录到达目标点的DOM个数,用于判断是否是否DOM都到达目标点 6 for(var i = 0; i < len; i++){ 7 startMove(self[i],json,speed,easing,function(){ 8 times++; 9 if(times == len){ //如果所有DOM动画执行完毕,调用回调函数执行 10 callback && callback(); 11 next(); //所有DOM执行完动画后,并且回调函数执行完,执行动画队列的下一个动画 12 } 13 }); 14 } 15 } 16 this.myQueue('fx',baseFunc); 17 if(this.myQueue('fx').length == 1){ 18 this.myDequeue('fx'); 19 }
其实animate()方法非常的简单,本质上就是对队列的应用,先将动画需要的参数和动画函数合并到一个方法内(baseFunc),并且这个方法带有形参next。然后将这个方法添加到每个DOM的动画队列(“fx”)中(16行)。接着18行就将这个刚刚添加到队列中的函数进行出队操作,animate方法末尾有放回this,所以又会接着操作一下个animate方法,链式调用时jQuery的基本特性,我在仿写的jQuery中也有实现。
需要注意的是运动函数有个地方需要改一下(相对定点定时运动):
if(!easing){ easing = jQuery.easingObj.swing; }else{ easing = jQuery.easingObj[easing]; }
因为运动函数调用时this指向是window,如果将easingObj放到animate方法内部的话没办法获取,所以在jQuery对象上定一个属性为easingObj,然后在运动函数中通过jQuery对象来调用,在jQuery源码中也是这么操作的。接着下面是动画延迟方法myDelay()的源码:
1 //动画延迟 2 jQuery.prototype.myDelay = function(duration){ 3 var len = this.length; 4 var queueArr = this[len-1].queueObj['fx']; 5 queueArr.push(function(next){ 6 setTimeout(function(){ 7 next(); 8 },duration); 9 }); 10 return this; 11 }
这个实现起来也是非常的简单,本质上就是在方法内设置一个定时器,定时器的回调函数内写入next执行,然后将这个方法添加到fx队列中,当定时器延迟时间一到就执行下一个动画函数。
最关键的原理:定时器的异步成就了animate()方法的链式调用,不然你想想如果没有异步,后面的动画是怎么被添加进去的,这里的异步应用才是animate的法宝,当第一个animate被执行添加第一个动画后就马上被进行出列操作,那后面的动画是怎么被放到队列中的呢?因为当第一次出队时动画进入了异步程序,所以并没有阻塞在第一个animate,而是动画进入异步执行,在动画异步倒计时的时候,animate动画函数的链式调用早就快速的执行完毕了,所以当第一个动画执行完以后,可以直接使用next()方法做出队操作。
//用下面这部分代码来理解这个最核心的原理(用来理解异步程序) $(".demo")[0].obj = [function(){console.log(1)}]; $(".demo")[0].aa = function(){ var a = this; var sum = 0; var time = setInterval(function(){ a.obj[sum](); sum++; if( sum == 2 ){ clearInterval(time); } },3000); return this; } $(".demo")[0].aa().obj.push(function(){console.log(2)});