一直以來,JavaScript的動畫都是通過定時器和間隔來實現的。雖然使用CSS transitions 和 animations使Web開發實現動畫更加方便,但多年來以JavaScript爲基礎來實現動畫卻很少有所改變。直到Firefox 4的發佈,才帶來了第一種對JavaScript動畫的改善的方法。但要充分認識改善,這有利於幫助我們瞭解web動畫是如何演變改進的。
定時器Timer
用於創建動畫的第一個模式是使用鏈式setTimeout()調用。在Netscape 3′s hayday的很長一段時期,開發者都記得一種在網絡上隨處可見的固定式最新行情狀態欄,通常它類似於這樣:
- (function(){
- var msg = "新的廣告",
- len = 25,
- pos = 0,
- padding = msg.replace(/./g, " ").substr(0,len),
- finalMsg = padding + msg;
- function updateText(){
- var curMsg = finalMsg.substr(pos++, len);
- window.status = curMsg;
- if (pos == finalMsg.length){ pos = 0; }
- setTimeout(updateText, 100);
- }
- setTimeout(updateText, 100);
- })();
標籤用來模擬window.status,例如:newsticker example 這種讓人煩惱的web模式,後來遭到對window.status禁用的抵抗,但隨着Explorer 4和Netscape 4的發佈,瀏覽器第一次給開發者更多對頁面元素的控制權限,這種技術再次出現。這樣就出現了使用javascript動態改變元素大小、位置、顏色等的一種全新動畫模式。例如,下面就是一個將div寬度變化成100%的動畫(類似於進度條):
- (function(){
- function updateProgress(){
- var div = document.getElementByIdx_x("status");
- div.style.width = (parseInt(div.style.width, 10) + 5) + "%";
- if (div.style.width != "100%"){ setTimeout(updateProgress, 100); }
- }
- setTimeout(updateProgress, 100);
- })();
儘管動畫在頁面上的地方不同,但基本原理卻是一樣的:做出改變,用setTimeout()間隔使頁面更新,然後setTimeout又執行下一次變化,這個過程反覆執行,直到動畫完成(見進度條動畫),早期的狀態欄動畫是相同的技術,只是動畫不一樣而已。 間隔動畫Intervals 隨着成功將動畫引入web,新的探索開始了。一個動畫已經無法滿足了,現在需要多個動畫。首次嘗試爲每個動畫創建多個動畫循環,在早期的瀏覽器中使用setTimeout()來創建多個動畫是有點複雜的,所以開發商開始使用setInterval()一創建單一的動畫循環,來管理頁面上所有的動畫,一個使用wetInterval()的基本動畫像這樣:
- (function(){
- function updateAnimations(){
- updateText();
- updateProgress();
- }
- setInterval(updateAnimations, 100);
- })();
創建一個小動畫庫,updateAnimations()方法將每一個動畫(同時看到一個新聞股票和進度條在一起運行)循環執行並進行適當的改變。如果沒有動畫需要更新,該方法可以退出而不做任何事情,甚至停止動畫循環,直到有更多的動畫更新做好準備。 動畫問題比較棘手的問題是延遲應該爲多少。間隔一方面必須足夠短,從而使不同的動畫都能流暢的進行,別一方面還要足夠長,使得瀏覽器可以完成渲染。大多數瀏覽器的刷新頻率爲60HZ,即每秒60次刷新,大多數瀏覽器的刷新頻率都不會比這個更頻繁,因爲他們知道,最終用戶是得不到更好的體驗的。 鑑於此,爲流暢動畫的最佳時間間隔爲1000毫秒/ 60,約17ms。在這個頻率你會看到流暢的動畫,那是因爲你最大的接近了瀏覽器能達到的頻率。跟以前的動畫相比,你會發現17ms間隔的動畫更加平滑,也更快(因爲動畫更新更頻繁,沒有做其他任何修改的情況下),多個動畫可能需要節流,以免17ms的動畫完成得太快。 問題 即使使用setInterval()爲基礎的動畫循環比多套使用setTimeout()的動畫循環高效,這裏還是存在問題。無論是setInterval()還是setTimeout()都無法達到精確,這個延遲即你指定的第二個參數僅僅表示何時代碼會添加到瀏覽器的可能被執行的UI線程隊列中。如果隊列中有其他工作在此之前,那代碼將會等到他完成纔會執行。簡而言之,毫秒級的延遲不是表示何時代碼會執行,而是表示何時代碼會添加進隊列。如果UI線程處於繁忙狀態或在處理用戶動作,那麼代碼將不會被馬上執行。 平滑動畫的關鍵是理解下一幀何時被執行,直到現在都沒有一個方法來保證下一幀將會在瀏覽器中被繪製。隨着的日益流行和新的基於瀏覽器的遊戲的出現,開發商對setInterval()和setTimeout()的不精準越來越感到失望。 瀏覽器的計時器分辨率加劇了這個問題,計時器對毫秒不精準,這裏有一些常見的計時器分辨率: Internet Explorer 8 and earlier 15.625ms Internet Explorer 9 and later 4ms. Firefox and Safari ~10ms. Chrome has a timer 4ms. IE在版本9之前的的分辨率爲15.625,所以0~15之間的任意值可能是0或15,但沒有分別。IE9的計時器分辨率改進爲4ms,但涉及到動畫時也是不具體的,chrome的計時器分辨率爲4ms,firefox 和 safari的爲10ms。因此即使你把間隔設定爲最佳的顯示效果,你也僅僅是得到這個近似值。 mozRequestAnimationFrame Mozilla 的 Robert O’Callahan 在思考這個問題,並想出了一個獨特的方案。他指出CSS transitions 和 animations的優勢在於瀏覽器知道哪些動畫將會發生,所以得到正確的間隔來刷新UI。而javascript動畫,瀏覽器不知道動畫正在發生。他的解決方案是創建一個mozRequestAnimationFrame()方法來告訴瀏覽器哪些javascript代碼正在執行,這使得瀏覽在執行一些代碼後得到優化。 mozRequestAnimationFrame()方法接受一個參數,是一個屏幕重繪前被調用的函數。這個函數用來對生成下合適的dom樣式的改變,這些改變用在下一次重繪中。你可以像調用setTimeout()一樣的方式鏈式調用mozRequestAnimationFrame(),例如:
- function updateProgress(){
- var div = document.getElementByIdx_x("status");
- div.style.width = (parseInt(div.style.width, 10) + 5) + "%";
- if (div.style.left != "100%"){
- mozRequestAnimationFrame(updateProgress);
- }
- }
- mozRequestAnimationFrame(updateProgress);
由於mozRequestAnimationFrame()只運行給定的函數一次,你需要在下一次UI動畫的時候再次調用它。你也需要相同的方法來管理何時停止調用。很酷,是非常流暢的動畫增強的實例。 因此,mozRequestAnimationFrame()解決了瀏覽器不知道Javascript動畫正在執行和不知道多少纔是合適的間隔的問題,但對於不知道何時你的代碼才被真正執行,也是由這個方案來解決的。 傳遞給mozRequestAnimationFrame()的函數實際是一個下一次重繪何時發生的的時間碼(以毫秒爲單位自1970年1月1日計算)。這是很重要的一點:mozRequestAnimationFrame()實際上列表出將要重繪的點並可以告訴你他們所處的時間。這樣你就能夠決定怎樣更好的來調整你的動畫。 爲了得到上次重繪過去的時間,你可以查詢mozAnimationStartTime,其中包含了過去重繪的時間代碼。減去傳遞迴調時的這個值可以計算出下一次重繪到屏幕時所用的時間。使用這些值的典型模式如下:
- function draw(timestamp){
- //calculate difference since last repaint
- var diff = timestamp - startTime;
- //use diff to determine correct next step
- //reset startTime to this repaint
- startTime = timestamp;
- //draw again
- mozRequestAnimationFrame(draw);
- }
- var startTime = mozAnimationStartTime;
- mozRequestAnimationFrame(draw);
關鍵是第一次不是通過callback調用時,mozAnimationStartTime是到mozRequestAnimationFrame()經過的時間。如果是在回調函數中,mozAnimationStartTime是通過參數傳遞進來的時間代碼平均值。 webkitRequestAnimationFrame 在很多人熱忠於chrome時,隨即創建了webkitRequestAnimationFrame()方法。這個版本與firefox的版本在兩方面有着細微的差別。一方面,它不通過回調函數傳遞時間代碼,你將無法知道下次重繪何時發生,另一方面,它添加了第二個可選參數來確定哪一個DOM元素髮生改變。因此,如果你知道重繪發生在頁面哪個部分的元素內,你可以限制重繪發生的區域。 應該不會感到驚訝,有沒有相應的mozAnimationStartTime,因爲如果沒有下一個重繪的時間信息不是很有益。有,只是webkitCancelAnimationFrame()取消了之前計劃的重繪。 如果你不需要精確的時間差異,你可以用下面的方式來創建一個用於Firefox4和chrome10+的動畫:
- function draw(timestamp){
- //calculate difference since last repaint
- var drawStart = (timestamp || Date.now()),
- diff = drawStart - startTime;
- //use diff to determine correct next step
- //reset startTime to this repaint
- startTime = drawStart;
- //draw again
- requestAnimationFrame(draw);
- }
- var requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame,
- startTime = window.mozAnimationStartTime || Date.now();
- requestAnimationFrame(draw);
這種模式使用可用的方法來創建以花費多少時間爲理念的循環動畫。Firefox使用時間代碼信息是有用的,而Chrome默認爲欠精準的時間對象。當用這種模式的時候,時間的差異給你一種多少時間過去了的想法,但不會告訴你Chrome的下一次重繪出現在何時。不過這比只有多少時間過去了的模糊概念要好些。 總結 mozRequestAnimationFrame()方法的介紹爲推動Javascript 動畫及web的歷史發展有着非常重要的作用。如前所述,JavaScript動畫的態幾乎和JavaScript的初期一樣。隨着瀏覽器逐漸推出CSS transitions 和 animations,很高興看到基於JavaScript的動畫的關注,因爲這些在基於的遊戲領域將變得更重要和更與CUP聯繫緊密。知道Javascript何時嘗試動畫,允許瀏覽器做更多的優化處理,包括在tab處於後臺或移動設備電量過低時停止進程。 該requestAnimationFrame()API現在正由W3C起草一個新議案,並正由Mozilla和Google努力使之成爲Web大舞臺的一部分。很高興能看到這兩大集團這麼迅速的兼容(可能不完全)實現。 RequestAnimFrame使用 對於一個偵中對DOM的所有操作,只進行一次Layout和Paint。 如果發生動畫的元素被隱藏了,那麼就不再去Paint。
- window.requestAnimFrame = (function(){
- return window.requestAnimationFrame ||
- window.webkitRequestAnimationFrame ||
- window.mozRequestAnimationFrame ||
- window.oRequestAnimationFrame ||
- window.msRequestAnimationFrame ||
- function( callback ){
- window.setTimeout(callback, 1000/60);
- };
- })();
- //調用
- function animationLoop(elem){
- requestAnimFrame(animationLoop);
- //logic
- }
Or
- window.requestAnimFrame = (function(w, r) {
- w['r'+r] = w['r'+r] || w['webkitR'+r] || w['mozR'+r] || w['msR'+r] || w['oR'+r] || function(c){ w.setTimeout(c, 1000 / 60); };
- return w['r'+r];
- })(window, 'equestAnimationFrame');