- 開篇一張圖之隊列模型
- 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)});