天天看點

requestAnimationFrame 知多少?

在Web應用中,實作動畫效果的方法比較多,JavaScript 中可以通過定時器 setTimeout 來實作,css3 可以使用 transition 和 animation 來實作,html5 中的 canvas 也可以實作。除此之外,html5 還提供一個專門用于請求動畫的 API,即  requestAnimationFrame(rAF),顧名思義就是 “請求動畫幀”。 為了深入了解  rAF 背後的原理(後文的 rAF 均指的是 requestAnimationFrame),我們首先需要了解一下與之相關的幾個概念:

螢幕繪制頻率

即圖像在螢幕上更新的速度,也即螢幕上的圖像每秒鐘出現的次數,它的機關是赫茲(Hz)。 對于一般筆記本電腦,這個頻率大概是60Hz, 可以在桌面上 右鍵 > 螢幕分辨率 > 進階設定 > 螢幕 中檢視和設定。這個值的設定受螢幕分辨率、螢幕尺寸和顯示卡的影響,原則上設定成讓眼睛看着舒适的值都行。

市面上常見的顯示器有兩種,即 CRT 和 LCD, CRT 是一種使用陰極射線管(Cathode Ray Tube)的顯示器,LCD 就是我們常說的液晶顯示器( Liquid Crystal Display)。

CRT 是一種使用陰極射線管的顯示器,螢幕上的圖形圖像是由一個個因電子束擊打而發光的熒光點組成,由于顯像管内熒光粉受到電子束擊打後發光的時間很短,是以電子束必須不斷擊打熒光粉使其持續發光。電子束每秒擊打熒光粉的次數就是螢幕繪制頻率。

而對于 LCD 來說,則不存在繪制頻率的問題,因為 LCD 中每個像素都在持續不斷地發光,直到不發光的電壓改變并被送到控制器中,是以 LCD 不會有電子束擊打熒光粉而引起的閃爍現象。

是以,當你對着電腦螢幕什麼也不做的情況下,顯示器也會以每秒60次的頻率正在不斷的更新螢幕上的圖像。為什麼你感覺不到這個變化? 那是因為人的眼睛有視覺停留效應,即前一副畫面留在大腦的印象還沒消失,緊接着後一副畫面就跟上來了,這中間隻間隔了16.7ms(1000/60≈16.7), 是以會讓你誤以為螢幕上的圖像是靜止不動的。而螢幕給你的這種感覺是對的,試想一下,如果重新整理頻率變成1次/秒,螢幕上的圖像就會出現嚴重的閃爍,這樣就很容易引起眼睛疲勞、酸痛和頭暈目眩等症狀。

CSS 動畫原理

根據上面的原理我們知道,你眼前所看到圖像正在以每秒 60 次的頻率繪制,由于頻率很高,是以你感覺不到它在繪制。而 動畫本質就是要讓人眼看到圖像被繪制而引起變化的視覺效果,這個變化要以連貫的、平滑的方式進行過渡。 那怎麼樣才能做到這種效果呢? 

60Hz 的螢幕每 16.7ms 繪制一次,如果在螢幕每次繪制前,将元素的位置向左移動一個像素,即1px,這樣一來,螢幕每次繪制出來的圖像位置都比前一個要差1px,你就會看到圖像在移動;而由于人眼的視覺停留效應,目前位置的圖像停留在大腦的印象還沒消失,緊接着圖像又被移到了下一個位置,這樣你所看到的效果就是,圖像在流暢的移動。這就是視覺效果上形成的動畫。 

setTimeout

了解了上面的概念以後,我們不難發現,setTimeout 其實就是通過設定一個間隔時間來不斷的改變圖像的位置,進而達到動畫效果的。但我們會發現,利用 seTimeout 實作的動畫在某些低端機上會出現卡頓、抖動的現象。 這種現象的産生有兩個原因:

  • setTimeout 的執行時間并不是确定的。在JavaScript中, setTimeout 任務被放進了異步隊列中,隻有當主線程上的任務執行完以後,才會去檢查該隊列裡的任務是否需要開始執行,是以 setTimeout 的實際執行時機一般要比其設定的時間晚一些。
  • 重新整理頻率受 螢幕分辨率 和 螢幕尺寸 的影響,不同裝置的螢幕繪制頻率可能會不同,而 setTimeout 隻能設定一個固定的時間間隔,這個時間不一定和螢幕的重新整理時間相同。

以上兩種情況都會導緻 setTimeout 的執行步調和螢幕的重新整理步調不一緻,進而引起丢幀現象。 那為什麼步調不一緻就會引起丢幀呢? 

首先要明白,setTimeout 的執行隻是在記憶體中對元素屬性進行改變,這個變化必須要等到螢幕下次繪制時才會被更新到螢幕上。如果兩者的步調不一緻,就可能會導緻中間某一幀的操作被跨越過去,而直接更新下一幀的元素。假設螢幕每隔16.7ms重新整理一次,而setTimeout 每隔10ms設定圖像向左移動1px, 就會出現如下繪制過程(表格):

  • 第    0  ms:螢幕未繪制,  等待中,setTimeout 也未執行,等待中;
  • 第   10 ms:螢幕未繪制,等待中,setTimeout 開始執行并設定元素屬性 left=1px;
  • 第 16.7 ms:螢幕開始繪制,螢幕上的元素向左移動了 1px, setTimeout 未執行,繼續等待中;
  • 第   20 ms:螢幕未繪制,等待中,setTimeout 開始執行并設定 left=2px;
  • 第   30 ms:螢幕未繪制,等待中,setTimeout 開始執行并設定 left=3px;
  • 第33.4 ms:螢幕開始繪制,螢幕上的元素向左移動了 3px, setTimeout 未執行,繼續等待中;
  • ...

從上面的繪制過程中可以看出,螢幕沒有更新 left=2px 的那一幀畫面,元素直接從left=1px 的位置跳到了 left=3px 的的位置,這就是丢幀現象,這種現象就會引起動畫卡頓。

rAF

與 setTimeout 相比,rAF 最大的優勢是 由系統來決定回調函數的執行時機。具體一點講就是,系統每次繪制之前會主動調用 rAF 中的回調函數,如果系統繪制率是 60Hz,那麼回調函數就每16.7ms 被執行一次,如果繪制頻率是75Hz,那麼這個間隔時間就變成了 1000/75=13.3ms。換句話說就是,rAF 的執行步伐跟着系統的繪制頻率走。它能保證回調函數在螢幕每一次的繪制間隔中隻被執行一次,這樣就不會引起丢幀現象,也不會導緻動畫出現卡頓的問題。

這個API的調用很簡單,如下所示:

var progress = 0;
 //回調函數
 function render() {
     progress += 1; //修改圖像的位置

     if (progress < 100) {
            //在動畫沒有結束前,遞歸渲染
            window.requestAnimationFrame(render);
     }
 }

 //第一幀渲染
 window.requestAnimationFrame(render);
      

除此之外,rAF 還有以下兩個優勢:

CPU節能:使用 setTimeout 實作的動畫,當頁面被隐藏或最小化時,setTimeout 仍然在背景執行動畫任務,由于此時頁面處于不可見或不可用狀态,重新整理動畫是沒有意義的,而且還浪費 CPU 資源。而 rAF 則完全不同,當頁面處理未激活的狀态下,該頁面的螢幕繪制任務也會被系統暫停,是以跟着系統步伐走的 rAF 也會停止渲染,當頁面被激活時,動畫就從上次停留的地方繼續執行,有效節省了 CPU 開銷。

函數節流:在高頻率事件(resize,scroll 等)中,為了防止在一個重新整理間隔内發生多次函數執行,使用 rAF 可保證每個繪制間隔内,函數隻被執行一次,這樣既能保證流暢性,也能更好的節省函數執行的開銷。一個繪制間隔内函數執行多次時沒有意義的,因為顯示器每16.7ms 繪制一次,多次繪制并不會在螢幕上展現出來。

優雅降級

由于 rAF 目前還存在相容性問題,而且不同的浏覽器還需要帶不同的字首。是以需要通過優雅降級的方式對 rAF 進行封裝,優先使用進階特性,然後再根據不同浏覽器的情況進行回退,直止隻能使用 setTimeout 的情況,是以可以這麼寫:

window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          function( callback ){
            window.setTimeout(callback, 1000 / 60);
          };
})();
      

但這種寫法沒有考慮 cancelAnimationFrame 的相容性,并且不是所有的裝置繪制時間間隔都是1000/60,下面的代碼是比較全的一個 polyfill,詳情介紹請參考: 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;
    }
}());
      

原創釋出 @一像素 2017.06.26

繼續閱讀