大家應該都知道,如果一個頁面運行的定時器很多,無論你怎么優化,最后肯定會超過指定時間才能完成動畫。定時器越多,延時越嚴重。
為此,YUI,kissy等采用中央隊列的方式,將定時器減少至一個。瀏覽器廠商也因此原生支持了requestAnimationFrame方法,此方法基本上能保證每秒刷新60次。但是此方法在還沒形成標准之前,很多低版本瀏覽器是不支持的,比如:IE9以及以下版本,不過谷歌和火狐都用私有的方法名實現了requestAnimationFrame方法。比如:谷歌:webkitRequestAnimationFrame,,火狐:mozRequestAnimationFrame。形成標准后,IE10才開始支持,由於IE10支持的是標准的requestAnimationFrame方法,因此它沒有私有前綴,所以並不存在msRequestAnimationFrame。
我們先來看一下requestAnimationFrame方法是如何使用的?
var startTime,duration = 3000,requestID;
function animate(now){ //webkitRequestAnimationFrame方法會給回調函數中傳入一個當前時間的參數。
var per = (now - startTime) / duration;
if(per >=1){
//動畫結束
}else{
div.style.left = Math.round(600*per) + "px";
window.webkitRequestAnimationFrame(animate); //此方法調用一次只會重繪一次動畫,如果需要連續的動畫,則需要重復調用
}
}
function start(){
startTime = Date.now();
requestID = window.webkitRequestAnimationFrame(animate); //此方法可以傳入兩個參數,第一個是回調,第二個是執行動畫的元素節點(可選),返回一個ID。
}
div.onclick = start;
上面的這個例子,是針對chrome瀏覽器實現的。
那么,我們如何來寫出兼容性的寫法呢?
第一個版本:
window.requestAnimationFrame = (function(){
return window.requestAnimationFrame || //IE10以及以上版本,以及最新谷歌,火狐版本
window.webkitRequestAnimationFrame || //谷歌老版本
window.mozRequestAnimationFrame || //火狐老版本
function(callback){ //IE9以及以下版本
window.setTimeout(callback , 1000/60); //這里強制讓動畫一秒刷新60次,這里之所以設置為16.7毫秒刷新一次,是因為requestAnimationFrame默認也是16.7毫秒刷新一次。
}
})();
上面這個兼容性寫法,有幾個問題,第一個:沒有解決cancelAnimationFrame方法的兼容性寫法。第二個:強制讓IE9-瀏覽器,動畫繪制間隔為16.7ms,但是這些瀏覽器的繪制間隔並不都是這個值。第三個:火狐老版本的mozRequestAnimationFrame方法跟標准的requestAnimationFrame方法實現有些出入,比如:早期火狐的此方法,不支持傳參。第四個:老版本的webkit,在有些版本下,此方法不會返回id,還有一些版本沒有給回調函數傳當前時間的參數。
我的理解:至於老版本的火狐和老版本的webkit,我個人覺得沒有必要去兼容,只要兼容IE9-瀏覽器就OK了。因此以上的4個問題,只存在前面兩個。那如果你想兼容第三個和第四個問題的話,請去看司徒正美基於網友屈屈與月影的版本改進而來的版本:https://github.com/wedteam/qwrap-components/blob/master/animation/anim.frame.js。
第二個版本,解決上面的第一個問題和第二個問題:
(function() {
var lastTime = 0;
var version = ['webkit', 'moz'];
for(var i = 0; i < version.length && !window.requestAnimationFrame; i++) { //如果此瀏覽器不支持requestAnimationFrame方法,就循環遍歷version數組
window.requestAnimationFrame = window[version[i] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[version [i] + 'CancelAnimationFrame'] || // 有一些Webkit版本中,此方法的名字改變了
window[version [i] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) { //如果是IE9-瀏覽器
window.requestAnimationFrame = function(callback, element) { //我們使用上一個例子來講解這段代碼。當我們點擊div時,就會觸發start方法,我們假設當前時間為11111,設置startTime=11111, 調用requestAnimationFrame(animate)方法,這時,當前時間,我們假設是currTime = 11112,lastTime = 0,這時timeToCall = 0,因此調用setTimeout(function(){},0),把lastTime = 11111,返回id。過了瀏覽器的最小時間后,我們假設是4ms,就會立即執行animate(11112)。這時就會繼續執行requestAnimationFrame。假設當前時間是11118,timeToCall = 9.7,這時lastTime = 11127.7,當前時間為11127.7時,就執行animate(11127.7),per = 16.7 / 3000,繼續執行requestAnimationFrame....
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); //timeToCall的值為0-16.7之間。
var id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
})();
當然,requestAnimationFrame不是沒有缺點,它不能控制fps(60),我們在以下場景下就不能使用。比如:做一些慢放動作,fps<60的情況下;還有在動作,槍戰,飛車等場景下,fps需要>60的情況下,像這種場景下,如果幀數不高,畫面會模糊。利用setTimeout(IE9,10,Firefox,chrome等,它的最短時間間隔已經壓縮至4ms了)我們可以輕松跑到100幀以上的動畫,能讓畫面更清楚,細節更逼真。
另外,postMessage這個異步方法,能實現超高度的動畫,有人做過實驗:
setTimeout 平均幀數200
requestAnimationFrame 平均幀數60
loop(循環) 平均幀數200-300
postMessage 平均幀數900-1000
var testing = true; //用來停止動畫的,也就是停止代碼執行的
function main(){
//記錄兩次執行時間的間隔
}
function run1(){ //點擊按鈕1,執行run1方法,然后使用setTimeout方法不斷的執行main方法,main方法會記錄每次執行的時間,求出兩次執行時間的間隔。
main();
if(testing){
setTimeout(run1, 1);
}
}
function run2(){ //點擊按鈕2,執行run2方法
main();
if(testing){
window.requestAnimationFrame(run2);
}
}
function run3(){ //點擊按鈕3,執行run3方法
var count = 15;
while(count--){ //利用while循環執行main方法,記錄兩個循環操作之間的時間間隔。
main();
}
if(testing){
setTimeout(run3,1); //當然這里會有一點點的誤差,因為用到了setTimeout方法,這樣我們可以設置testing=false,停止循環調用main,如果直接用while(true),那么無法停止此循環。
}
}
window.addEventListener("message",run4,false); //綁定message事件,只要調用postMessage方法,就會觸發message事件。
function run4(){
main();
if(testing){
window.postMessage("","*");
}
}
IE10也有一個高效的異步方法setImmediate。
在現實中,尤其是游戲開發,我們要結合多種異步API。比如:作為背景的樹木,流水等用requestAnimationFrame方法,玩家角色,由於需要速度的變化,那么用setTimeout比較合適,一些非常炫的動畫,可能就需要postMessage,setImmediate,Image.onerror等API了。
加油!
