RAF是什麼東東

開篇

我們之前在這篇文章裏面講過瀏覽器的事件循環,還提到事件隊列,調用棧等瀏覽器的一些實現機制。但還有一些細節我們沒有提到,這篇文章我們就來把這些細節補充。

幀和動畫

你一定知道動畫片是怎樣製作的,沒錯,只需要很多張畫滿動畫的紙張,只要這些紙張的動畫情景是按照時間的連續性排列,那麼他們按照一定的速度在你的眼前切換,你就能看到一部完整的動畫片了。我們的電腦播放的各種操作動畫也是一樣的道理。你端坐在電腦面前的時候,gpu就是不斷地在屏幕上繪製圖片纔會讓你覺得電腦真的是在動起來了。人的眼睛如果在1秒之內看到有超過60張圖片在切換,那麼我們就相當於看到了一部動畫一樣。在能看到動畫最低的動畫是60幀,就是1面內60次的連續動作,我們會認爲這個動畫是流暢的。按照計算我們可以得出1000/60 = 16.6ms爲一幀(frame)。大多數電腦也是按照這個速率刷新我們的屏幕。

渲染流水線

和電腦一樣,瀏覽器也是按照這個頻率把網頁上的元素的變化反饋給GPU的。如果屏幕的刷新率60fps,那麼我們的js腳本執行一次我們就是重繪一次界面會不會太浪費了,因爲js腳本的執行大多數時候非常短暫。爲了避免這種不必要的浪費,我們的瀏覽器是按照電腦刷新頻率執行繪製的界面的任務。也就是說假設我們改變dom的腳本時間只用了1ms,那麼需要等待一段時間10~15ms纔會執行我們的繪製界面任務工作。這期間消息隊列中會是空的,不會有渲染相關的任務被推入消息隊列被執行。我們操作dom的場景一般如下代碼所示:

document.getElementById("box").style = "height: 100px";

這段代碼修改了界面上一個id爲box的樣式,我們雖然只執行了一條簡短的語句,但是瀏覽器卻爲我們做了很多事情:

  • 執行javascript腳本
  • 計算界面元素的css樣式
  • 重新計算界面元素的佈局
  • 開始開始繪製界面
  • 合成層(如果有需要的話)

49.png
Javascript操作dom時產生的變化是需要瀏覽器執行界面的繪製任務才能被更新到屏幕上的,在事件循環中,我們依次將這些任務入隊,然後執行。但是我們之前提到過,瀏覽器的刷新率是60fps,也就是說,後面三步並不是總是接着javascript腳本的執行而執行的,而是需要等待屏幕刷新之前執行這三個任務。一般來說這個時間是大概是16.6ms,也就是屏幕刷新率60fps。所以,我們的腳本執行如果非常快的話,那麼操作結果就會“等待”屏幕刷新纔會被用戶看到,這個等待時間非常短,對於人的眼睛來說完全不會有延遲,但是對於高速運轉的計算機,可以節約很多繪製界面的任務和資源。

定時器

使用settimeout或者setinterval來渲染動畫存在一些問題,首先就是他們最小間隔時間是4.7ms,而並非是你指定的0,所以當你用settimeout 0 來執行你的動畫,你會發現實際上移動的速度是要比預期的執行速度快的。我們之前說過,界面繪製的速率是16.6ms左右,而定時器最小執行時間是4.7ms,所以,在執行了3-4次定時器函數之後,我們才能看到一個片段被繪製在屏幕上,正確的應該是給定時器設置16.6ms的時間,才正好與屏幕刷新率同步。

16.6ms這個時間只是我們推算出來的一個事件,定時器對這個時間只需並不準確,而卻隨着執行次數的推一,誤差會越來越遠離實際值

使用定時器運行動畫函數的另外一個缺點就是settimeout在每一幀的執行時間會受到其他任務和自身的影響,這種影響如果疊加,會影響到定時器在下一幀出現的位置。爲了形象地說明,我們來看下面這張圖。
50.png
從上面的圖來看,我們雖然可以確保定時器執行的間隔的時間,但是無法確定定時器執行時所在一幀的位置,而且在某些情況下,過長的執行時間會導致後面的繪製任務被推後,從而影響動畫執行的流程度。

總得來說使用定時器來執行動畫有以下幾種缺點:

  1. 定時器的計算過大會影響動畫的流暢度,而過小則影響動畫的速率。
  2. 定時器的延遲時間其實是並不精確,並且隨着時間推移精度會逐漸下降。
  3. 定時器的執行時機會被其他長任務影響,所以並不好準確控制每一幀的開始位置。

requestAnimationFrame

raf之所以會適合做動畫效果,其中一個很大的原因就是raf的執行速率與屏幕刷新的速率相同。瀏覽器會在下一次界面繪製之前執行raf函數。這個時間並不需要我們自己去指定,瀏覽器已經自己定義好了。我們可以看下面的圖:
52.png
可以看到,左邊是js腳本的循環賽道,右邊是raf、渲染流水線的賽道。左邊任務執行的頻率是實時的,也就是說一旦有任務被推入了消息隊列,立馬執行,而右邊則是有規律的執行,這個規律則是屏幕刷新律,也就是大概16.6ms就執行一次。所以在rAF中的代碼,執行速率是與定時器中的不一樣的。我們以一下代碼爲例:

el.style.display = "block";
el.style.display = "none";
el.style.display = "block";
el.style.display = "none";
el.style.display = "block";

如果你解了刷新原理,就能很好的回答上面的問題,由於js執行的非常快,上面的語句幾乎可以在1ms內執行完成,但是我們的繪製任務需要等待下一次屏幕刷新之前才能執行,因此我們只會看到最後一條js執行的結果,實際上界面不會有任何變化。
使用raf的另外一個好處就是因爲raf處在每一幀的最前面,所以有足夠多的剩餘時間去執行自身或者其他計算繪製的任務,保證所有的任務都在一幀內被執行,如圖所示:
51.png

與其他大多數瀏覽器不一樣,蘋果系統在處理raf函數時講這個函數執行順序放到了刷新界面之後,導致的問題就是有一幀無法被觀察到。

總結

這次我們介紹了定時器以及rAF函數,他們的工作原理以及實現動畫的優先選項。其實如果你深入瞭解過瀏覽器的一些渲染機制,就能很好地理解定時器和rAF函數的區別,以及爲什麼我們會優先選擇後者作爲動畫的執行的函數。不僅僅在dom上,我們在處理2d或者3d動畫的時候,都是採用的rAF函數讓計算機去計算每個物體的位置變化。在處理大數據視覺上我們更需要關心的性能,定時器顯然跟不上我們的需求。

參考文檔

What the heck is the event loop anyway? -Philip Roberts
In The Loop -Jake Archibald

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章