一、初識requestAnimationFrame
requestAnimationFrame解決了瀏覽器不知道javascript動畫什么時候開始攻晒、不知道最佳循環(huán)間隔時間的問題匾旭。它是跟著瀏覽器的繪制走的鹰溜,如果瀏覽器繪制間隔是16.7ms,它就按這個間隔繪制;如果瀏覽器繪制間隔是10ms, 它就按10ms繪制褪那。這樣就不會存在過度繪制的問題,動畫不會丟幀式塌。
內部是這么運作的:
瀏覽器頁面每次要重繪博敬,就會通知requestAnimationFrame;
這是資源非常高效的一種利用方式。
有以下兩點:
1峰尝、就算很多個requestAnimationFrame()要執(zhí)行偏窝,瀏覽器只要通知一次就可以了。而setTimeout是多個獨立繪制武学。
2祭往、一旦頁面不在當前頁面(比如:頁面最小化了),頁面是不會進行重繪的火窒,自然requestAnimationFrame也不會觸發(fā)(因為沒有通知)硼补。頁面繪制全部停止,資源高效利用熏矿。
二. 動畫的循環(huán)間隔
編寫動畫循環(huán)的關鍵已骇,是要知道延遲時間多長合適。一方面票编,循環(huán)時間必須足夠短褪储,這樣才能保證動畫效果更平滑流暢;另一方面慧域,循環(huán)還要足夠長鲤竹,這樣才能保證瀏覽器有能力渲染產生的變化。大多數(shù)顯示器的刷新頻率是60Hz昔榴,相當于每秒鐘重繪60次宛裕。大多數(shù)瀏覽器都會對重繪操作加以限制,不超過顯示器的重繪頻率论泛,因為即使超過了這個頻率揩尸,用戶體驗也不會有提升。
因此最平滑動畫的最佳循環(huán)間隔是1000ms/60屁奏,約等于17ms岩榆。以這個循環(huán)間隔重繪的動畫是平滑的,因為這個速度最接近瀏覽器的最高限速。為了適應17ms的循環(huán)間隔勇边,多重動畫可能需要加以節(jié)制犹撒,以便不會完成得太快。
雖然與使用多組setTimeout()
相比粒褒,使用setInterval()
的動畫循環(huán)效率更高识颊。但是無論setTimeout()
還是setInterval()
都不十分精確。為它們傳入的第二個參數(shù)奕坟,實際上只是指定了把動畫代碼添加到瀏覽器UI線程隊列以等待執(zhí)行的時間祥款。如果隊列前面已經加入了其他任務,那動畫代碼就要等前面的任務執(zhí)行完成后再執(zhí)行月杉。如果UI線程繁忙刃跛,比如忙于處理用戶操作,那么即使把代碼加入隊列也不會立即執(zhí)行苛萎。
因此桨昙,知道什么時候繪制下一幀是保證動畫平滑的關鍵。然而腌歉,面對不十分精確的setTimeout()
和setInterval()
蛙酪,開發(fā)人員至今都沒有辦法確保瀏覽器按時繪制下一幀。
以下是幾個瀏覽器的計時器精度
:
IE8及其以下版本瀏覽器: 15.6ms翘盖;
IE9及其以上版本瀏覽器:4ms桂塞;
Firefox和Safari:10ms;
Chrome:4ms最仑。
更為復雜的是藐俺,瀏覽器開始限制后臺標簽頁或不活動標簽頁的計數(shù)器。因此泥彤,即使你優(yōu)化了循環(huán)間隔欲芹,可能仍然只能接近你想要的效果。
三. requestAnimationFrame()
Mozilla的 Robert O'Callahan 指出吟吝,CSS變換動畫的優(yōu)勢在于瀏覽器知道動畫什么時候開始菱父,因此會計算出正確的循環(huán)間隔,在適當?shù)臅r候刷新UI剑逃。而對于JavaScript動畫浙宜,瀏覽器就無從知曉什么時候開始。
因此Robert O'Callahan的方案是蛹磺,創(chuàng)建一個新方法mozRequestAnimationFrame()
粟瞬,通過它告訴瀏覽器某些代碼將要執(zhí)行動畫。這樣瀏覽器可以在運行某些代碼后進行適當?shù)膬?yōu)化萤捆。
與setTimeout()
和setInterval()
方法不同裙品,requestAnimationFrame()
不需要調用者指定幀速率俗批,瀏覽器會自行決定最佳的幀效率。
requestAnimationFrame()
方法接收一個參數(shù)市怎,即在重繪屏幕前調用以個函數(shù)岁忘。這個函數(shù)負責改變下一次重繪時的DOM樣式。為了創(chuàng)建動畫循環(huán)区匠,可以像使用setTimeout()一樣干像,把多個對requestAnimationFrame()
的調用連綴起來。
如:
function drawFrame() {
window.requestAnimationFrame(drawFrame);
// animation code...
}
window.requestAnimationFrame(drawFrame);
四. requestAnimationFrame()的兼容性
4.1 requestAnimationFrame()的兼容性封裝:
由于mozRequestAnimationFrame()
是HTML5的新功能,目前各大瀏覽器的支持情況各異。如果希望代碼具備更好的跨平臺性,可以考慮使用下面的代碼實現(xiàn)各平臺兼容性:
if(!window.requestAnimationFrame) {
window.requestAnimationFrame = (window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
let self =this, start, finish;
return window.setTimeout(function() {
start = + new Date();
callback(start);
finish = + new Date();
self.timeout = 1000/60 - (finish - start);
}, self.timeout);
});
}
這段代碼先檢查了window.requestAnimationFrame
函數(shù)的定義是否存在。如果不存在漏健,就遍歷已知的各種瀏覽器實現(xiàn)并替代該函數(shù)。如果還是找不到一個與瀏覽器相關的實現(xiàn)雹熬,它最終會采用基于JavaScript定時器的動畫以每秒60幀的間隔調用setTimeout函數(shù)敬惦。
mozRequestAnimationFrame()
會接收一個時間碼(從1970年1月1日起至今的毫秒數(shù)),表示下一次重繪的實際發(fā)生時間辅愿。這樣智亮,mozRequestAnimationFrame()
就會根據(jù)這個時間碼設定將來的某個時刻進行重繪。
但是webkitRequestAnimationFrame()
和msRequestAnimationFrame()
不會給回調函數(shù)傳遞時間碼点待,因此無法知道下一次重繪將發(fā)生在什么時間阔蛉。
如果要計算兩次重繪的時間間隔,F(xiàn)irefox中可以使用既有的時間碼癞埠,而在Chrome和IE則可以使用不太精確地Date()對象状原。
4.2 cancelRequestAnimFrame()的兼容性封裝:
W3C也提供了cancelRequestAnimationFrame()
方法,用于取消回調函數(shù)苗踪。requestAnimationFrame()
方法會返回一個對象颠区,用做標識回掉函數(shù)身份。若要取消回調函數(shù)的執(zhí)行通铲,可將其傳給cancelRequestAnimationFrame()
毕莱。
window.cancelRequestAnimFrame = (function() {
return window.cancelAnimationFrame ||
window.webkitCancelRequestAnimationFrame ||
window.mozCancelRequestAnimationFrame ||
window.oCancelRequestAnimationFrame ||
window.msCancelRequestAnimationFrame ||
clearTimeout;
} )();
4.3 requestAnimationFrame()升級版封裝方法:
另外還有一種更優(yōu)雅的requestAnimationFrame()
的兼容性封裝方法:
(function() {
let lastTime = 0;
let vendors = ['ms', 'moz', 'webkit', 'o'];
for(let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
let currTime = new Date().getTime();
let timeToCall = Math.max(0, 1000/60 - (currTime - lastTime));
let id = window.setTimeout(function() {
callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if(!window.cancelAnimationFrame)
window.cancelAnimationFrame =function(id) {
clearTimeout(id);
};
}());