你知道 setTimout、setInterval、requestAnimationFrame API 三者的關係嗎

Nick
7 min readJun 14, 2020

前言

這不是面試題,是近期實作關於動畫的開發項目時,在被同事 Code Review 時有提及的一個議題,內容是在計算動畫更新的邏輯從 setInterval 觸發改成使用 requestAnimationFrame API會更好,因此小展開了 Web API 的研究之旅,所以請大家可以先找文章補充 setTimout、setInterval ( X

這是 requestAnimationFrame 的 polyfill 程式碼(在瀏覽器不支援時的替代程式)

看到專案 code base 裡模組去取得各瀏覽器 requestAnimationFrame API如取不到有實作 polyfill註一)時我有點小傻眼了,竟然是使用 setTimeout 並指定 delay time,心裡想著是不是被耍了,不經想著如果這樣也能稱為一個新 API ,那我再擴增寫 requestAnimationFrame 會 callback 自己,那不就變成 requestAnimationFrames 了,我真是天才 XD ,但事情不是這麼簡單的,讓我們看下去。

註一polyfill翻譯成中文是「自動補完函式庫」,是小段程式碼,用各種技術來幫忙各個舊的瀏覽器,補完新的 API 邏輯,讓你使用起來沒有落差。

不急,我們先來看個動畫案例:Clock Canvas

直接到 Codepen 上找一個 Clock 的專案,直接給他 fork 下去,原作者是 Marco

一個時鐘動畫的範例
程式碼架構

說明程式碼用途說明
initCanas:用來配置 Canvas 畫布樣式並取得物件
renderTime:用來計算時鐘呈現的畫面(時針分針位置)
setInterval:每 100ms 重複執行 renderTime

所以整串就會是,初始化使用 initCanvas,然後透過setInterval去每隔 100ms 不斷重複執行繪製時鐘畫面,最後形成這個 clock 動畫。

討論一:更新間隔應該設定幾毫秒 (ms)

以上三個動畫是在設定不同的間隔時間更新畫面,分為是 30ms、60ms、200ms,你可能會告訴我最右邊的 200ms 看起來很 lag,左邊兩個差不多,但我可以告訴你實際上三個看起來都很 lag ,因為我錄製 gif 圖片檔要把 FPS 調低才的傳上來 XD

但說到 FPS ,全名是 Frames Per Second,在大部分電腦、手機裝置的畫面渲染頻率都固定是 60 FPS,也就是說我們要在 1000 (s) / 60 (frame) 約為 16 (ms/frame) 內完成一次畫面更新的計算,就會達到最流暢的體驗,快於這個值可能就只會造成多餘的資源浪費,因為多計算的內容實際上也不會渲染出畫面給使用者看。

討論二:設定 setInterval 為 16 ms 執行一次就完美了嗎?

瀏覽器實際運行程式跟真正繪製的問題解說圖

先稍微解釋一下上圖中的每個 16ms 間隔是大部分瀏覽器固定重繪製的週期,跟程式邏輯中的動畫畫面計算邏輯是兩回事,程式邏輯計算也需要一個時計算畫面(如 clock 的時針分針位置),然後再交由瀏覽器的繪製週期觸發去實際繪製出來。

如討論標題使用 setInterval 配置每 16 ms 去執行重算動畫畫面的邏輯,在大部分情況是不會有問題,但是在一種情況可能有問題,就在 setInterval 的起始執行位置跟刷新畫面的 16ms 頻率交界很接近時,執行可能會發生一個情況就是 偶爾計算新畫面時期超過刷新週期 16ms 界線,而來不及被 render 的囧境

實際執行重繪程式邏輯在渲染週期的位置圖

使用 chrome Dev Tool 中的 performance 錄製 timeline 會發現 setInterval 被 fire 位置確實不一定會跟渲染週期起始點一致。

  • 上圖中間寫著 15.6 ms、 16.9 ms 就是每次實際的渲染週期。
  • 灰色區域是每隔 16ms 被 setInterval 觸發的重新計算畫面的邏輯,以下黃色紫色就是在更細部每個被執行計算的函數實際花費的時間。

使用 requestAnimationFrame API 能怎麼優化

requestAnimationFrame 就像是自動指定了 delay 的 setTimeout,觸發時間也不是固定的,實際上會依照當前裝置的渲染率,並且自動選在每個 Frame 的最佳位置去執行 callback 函數,讓動畫運算邏輯擁有最完整的間隔時間運算,以達到優化的效果,以下來實作看看。

先試著先把 setInterval 改成 setTimeout 的程式碼。

setTimeout 是一次執行,要達成重複就要遞迴自己,但這樣還沒解決問題

再把setTimeout 改成, requestAnimationFrame並移除 delay time

再改成 requestAnimationFrame 不用指定 delay 時間,它觸發時間是隨著渲染週期的觸發
重新錄製使用 requestAnimationFrame 的 timeline

再開錄一次 timeline ,可以發現 renderTime 的執行位置被自動調整在繪製週期前可執行完的時間呼叫。

調整後的畫面

小結

回到開頭提到的 setTimoutsetIntervalrequestAnimationFrame 三者關係

  • setTimeout 是用來指定一段時間時候執行一次指定邏輯的 Web API,如果要用 setTimeout 做重複,可能會遇到每次間隔要加上程式碼執行時間。
  • setInterval 是用來指定每隔一段時間執行一次指定邏輯的 Web API,重複觸發 setTimout 且在效能沒問題情況下,不會位移觸發的間隔時間。
  • requestAnimationFrame是優化動畫邏輯計算可用的 Web API,使用方法跟setTimeout很像,只是不用指定 dealy time,瀏覽器會依照渲染週期優化在渲染前足夠時間執行完指定計算畫面的邏輯。

在 Google Developer Docs 說到 針對視覺變更,請使用 RequestAnimationFrame的建議,整體上程式要調整成本也蠻少的,所以建議大家有用到不妨試試看這個 API 吧,雖然目前對我來說這 API 當作冷知識比實際帶來價值高許多,可能不是遊戲或很多重動畫體驗的產品網站應該不會感受到高計算效率的問題,在那個當下能完整使用 選染週期 的資源就變成一個很重要的課題,處理不好可能 FPS 從 60 掉成一半 30 也不一定。

資料來源

如果喜歡我這篇文,可以幫我拍手 1-10 下
如果覺得這文章對你有幫助,可以幫我拍手 10–30
如果覺得想支持 Web API 這主題文章,可以幫我拍手 30–50 讓我知道
也記得 Follow我 陳建宇
更歡迎你在下方留言,我很樂意與你討論聊天或回答問題!

--

--

Nick

嗨 我是 Nick,目前在 ShopBack 擔任軟體工程師,喜歡用科技解決問題,偶爾會整理一些工作上的發現分享在這,歡迎大家追蹤及交流