在Web應用中,實現動畫效果的方法比較多,Javascript 中可以通過定時器 setTimeout
來實現,css3 可以使用 transition
和 animation
來實現,html5 中的 canvas 也可以實現。除此之外,html5 還提供一個專門用於請求動畫的API,那就是 requestAnimationFrame
,顧名思義就是請求動畫幀。
一、相關概念
為了理解 requestAnimationFrame
背後的原理,我們首先需要了解一下與之相關的幾個概念:
1. 頁面可見
當頁面被最小化或者被切換成後臺標籤頁時,頁面為不可見,瀏覽器會觸發一個 visibilitychange
事件,並設定document.hidden
屬性為true
;切換到顯示狀態時,頁面為可見,也同樣觸發一個 visibilitychange
事件,設定document.hidden
屬性為false
。
2. 動畫幀請求回撥函式列表
每個Document都有一個動畫幀請求回撥函式列表,該列表可以看成是由<handlerId, callback>
元組組成的集合。其中handlerId
是一個整數,唯一地標識了元組在列表中的位置;callback是
回撥函式。
3. 螢幕重新整理頻率
即影象在螢幕上更新的速度,也即螢幕上的影象每秒鐘出現的次數,它的單位是赫茲(Hz)。 對於一般膝上型電腦,這個頻率大概是60Hz, 這個值的設定受螢幕解析度、螢幕尺寸和顯示卡的影響。
4. 動畫原理
根據上面的原理我們知道,你眼前所看到影象正在以每秒60次的頻率重新整理,由於重新整理頻率很高,因此你感覺不到它在重新整理。而動畫本質就是要讓人眼看到影象被重新整理而引起變化的視覺效果,這個變化要以連貫的、平滑的方式進行過渡。 那怎麼樣才能做到這種效果呢?
重新整理頻率為60Hz的螢幕每16.7ms重新整理一次,我們在螢幕每次重新整理前,將影象的位置向左移動一個畫素,即1px。這樣一來,螢幕每次刷出來的影象位置都比前一個要差1px,因此你會看到影象在移動;由於我們人眼的視覺停留效應,當前位置的影象停留在大腦的印象還沒消失,緊接著影象又被移到了下一個位置,因此你才會看到影象在流暢的移動,這就是視覺效果上形成的動畫。
二、 requestAnimationFrame用法
非同步,傳入的函式在重繪之前呼叫。
1. 寫法:handlerId = requestAnimationFrame(callback)
(1) 傳入一個callback
函式,即動畫函式;
(2) 返回值handlerId
為瀏覽器定義的、大於0的整數,唯一標識了該回撥函式在列表中位置。
2. 瀏覽器執行過程:
(1) 首先要判斷document.hidden
屬性是否為true
,即頁面處於可見狀態下才會執行;
(2) 瀏覽器清空上一輪的動畫函式;
(3) 這個方法返回的handlerId
值會和動畫函式callback
,以<handlerId , callback>
進入到動畫幀請求回撥函式列;
(4) 瀏覽器會遍歷動畫幀請求回撥函式列表,根據handlerId
的值大小,依次去執行相應的動畫函式。
3. 取消動畫函式的方法:
cancelAnimationFrame(handlerId)複製程式碼
三、setTimeout與requestAnimationFrame
1. setTimeout
理解了上面的概念以後,我們不難發現,setTimeout
其實就是通過設定一個間隔時間來不斷的改變影象的位置,從而達到動畫效果的。但利用seTimeout
實現的動畫在某些低端機上會出現卡頓、抖動的現象。 這種現象的產生有兩個原因:
setTimeout
的執行時間並不是確定的。在Javascript中,setTimeout
任務被放進了非同步佇列中,只有當主執行緒上的任務執行完以後,才會去檢查該佇列裡的任務是否需要開始執行,因此 setTimeout 的實際執行時間一般要比其設定的時間晚一些。- 重新整理頻率受螢幕解析度和螢幕尺寸的影響,因此不同裝置的螢幕重新整理頻率可能會不同,而
setTimeout
只能設定一個固定的時間間隔,這個時間不一定和螢幕的重新整理時間相同。
以上兩種情況都會導致setTimeout
的執行步調和螢幕的重新整理步調不一致,從而引起丟幀現象。 那為什麼步調不一致就會引起丟幀呢?
首先要明白,setTimeout
的執行只是在記憶體中對影象屬性進行改變,這個變化必須要等到螢幕下次重新整理時才會被更新到螢幕上。如果兩者的步調不一致,就可能會導致中間某一幀的操作被跨越過去,而直接更新下一幀的影象。假設螢幕每隔16.7ms重新整理一次,而setTimeout
每隔10ms設定影象向左移動1px, 就會出現如下繪製過程:
- 第0ms: 螢幕未重新整理,等待中,
setTimeout
也未執行,等待中; - 第10ms: 螢幕未重新整理,等待中,
setTimeout
開始執行並設定影象屬性left=1px; - 第16.7ms: 螢幕開始重新整理,螢幕上的影象向左移動了1px,
setTimeout
未執行,繼續等待中; - 第20ms: 螢幕未重新整理,等待中,
setTimeout
開始執行並設定left=2px; - 第30ms: 螢幕未重新整理,等待中,
setTimeout
開始執行並設定left=3px; - 第33.4ms: 螢幕開始重新整理,螢幕上的影象向左移動了3px,
setTimeout
未執行,繼續等待中; - …
從上面的繪製過程中可以看出,螢幕沒有更新left=2px的那一幀畫面,影象直接從1px的位置跳到了3px的的位置,這就是丟幀現象,這種現象就會引起動畫卡頓。
2. requestAnimationFrame
與setTimeout
相比,requestAnimationFrame
最大的優勢是由系統來決定回撥函式的執行時機。具體一點講,如果螢幕重新整理率是60Hz,那麼回撥函式就每16.7ms被執行一次,如果重新整理率是75Hz,那麼這個時間間隔就變成了1000/75=13.3ms,換句話說就是,requestAnimationFrame
的步伐跟著系統的重新整理步伐走。它能保證回撥函式在螢幕每一次的重新整理間隔中只被執行一次,這樣就不會引起丟幀現象,也不會導致動畫出現卡頓的問題。
這個API的呼叫很簡單,如下所示:
var progress = 0;
//回撥函式
function render() {
progress += 1; //修改影象的位置
if (progress < 100) { //在動畫沒有結束前,遞迴渲染
window.requestAnimationFrame(render);
}
}
//第一幀渲染
window.requestAnimationFrame(render);複製程式碼
除此之外,requestAnimationFrame
還有以下兩個優勢:
- CPU節能:使用
setTimeout
實現的動畫,當頁面被隱藏或最小化時,setTimeout
仍然在後臺執行動畫任務,由於此時頁面處於不可見或不可用狀態,重新整理動畫是沒有意義的,完全是浪費CPU資源。而requestAnimationFrame
則完全不同,當頁面處理未啟用的狀態下,該頁面的螢幕重新整理任務也會被系統暫停,因此跟著系統步伐走的requestAnimationFrame
也會停止渲染,當頁面被啟用時,動畫就從上次停留的地方繼續執行,有效節省了CPU開銷。
- 函式節流:在高頻率事件(
resize
,scroll
等)中,為了防止在一個重新整理間隔內發生多次函式執行,使用requestAnimationFrame
可保證每個重新整理間隔內,函式只被執行一次,這樣既能保證流暢性,也能更好的節省函式執行的開銷。一個重新整理間隔內函式執行多次時沒有意義的,因為顯示器每16.7ms重新整理一次,多次繪製並不會在螢幕上體現出來。
3. 優雅降級
由於requestAnimationFrame
目前還存在相容性問題,而且不同的瀏覽器還需要帶不同的字首。因此需要通過優雅降級的方式對requestAnimationFrame
進行封裝,優先使用高階特性,然後再根據不同瀏覽器的情況進行回退,直至只能使用setTimeout
的情況。下面的程式碼就是有人在github上提供的polyfill,詳細介紹請參考github程式碼 requestAnimationFrame。
if (!Date.now)
Date.now = function() { return new Date().getTime(); };
(function() {
'use strict';
var vendors = ['webkit', 'moz'];
for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
var vp = vendors[i];
window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];
window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame']
|| window[vp+'CancelRequestAnimationFrame']);
}
if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) // iOS6 is buggy
|| !window.requestAnimationFrame || !window.cancelAnimationFrame) {
var lastTime = 0;
window.requestAnimationFrame = function(callback) {
var now = Date.now();
var nextTime = Math.max(lastTime + 16, now);
return setTimeout(function() { callback(lastTime = nextTime); },
nextTime - now);
};
window.cancelAnimationFrame = clearTimeout;
}
}());
複製程式碼
(文章純屬個人備忘記錄用途,部分引用來自網上加上個人理解整理。歡迎轉載,請註明出處。如對你有幫助,請隨意打賞。)