現在這個年代如果要製作根據時間間隔來觸發的動畫,都會建議使用 requestAnimationFrame()
這個方法來做;跟 setInterval()
之類的方法相比,它至少有兩大好處:
- 瀏覽器內部實作會自動根據系統的能耐決定要間隔多久來觸發。
- 當網頁頁籤隱藏的時候,它會暫停觸發動畫,以節省運算成本和裝置電力。
通常在使用 requestAnimationFrame()
方法的時候,會建議以下面的基本結構來執行:
function draw() {
// 執行動畫的繪製動作...
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
由這個結構我們可以看出,requestAnimationFrame()
跟 setTimeout()
方法一樣是一次性的,如果要讓動畫繼續跑,就必須在回調(callback)函數的最後面再次自我請求。當然,這樣的自我請求並不會導致呼叫堆疊爆炸的問題,因為這邊回調函數的執行是非同步的,每次它被執行的時候都會被視為是全新的呼叫堆疊的開頭。
本來到此為止應該就沒有問題了,但是很快地,包括我在內有些人就注意到有一個情境之中這樣的執行結構有一點不太理想,那就是當我們開啟瀏覽器的偵錯工具、並且在裡面允許偵錯非同步呼叫堆疊的時候(預設就是開啟的)。這樣的一種偵錯模式會讓瀏覽器持續記錄所有非同步程式碼是從哪裡被觸發的,所以在上面那樣的結構之下,在偵錯工具的堆疊裡面就會顯示出像下面這樣沒完沒了的堆疊:
當然,即使這樣的堆疊記錄並不會真的因此導致任何當機,撇開個人的強迫症不提、它也一樣至少有兩個壞處:
- 雖然不知道瀏覽器內部實作原理如何,但是這種無盡的堆疊肯定怎樣也是在浪費記憶體。
- 如果回調函數裡面有丟出錯誤的話,在 console 中即時顯示的堆疊會被這些垃圾堆疊塞滿,製造偵錯上的麻煩。
在 three.js 的 Github 上頭就有一篇 issue 是在討論這個問題,所以顯然在我之前就有人在抱怨這件事了。該串裡面至少提到了兩種解法:
- 把非同步呼叫堆疊偵錯關閉。
可是這個功能本身其實在別的情境之中是很好用的,我並不想因此把它關掉。
- 不要在回調函數的最後自我呼叫,而改用一個很短的
setInterval()
去間接呼叫requestAnimationFrame()
。
這個作法的概念是企圖在保有 requestAnimationFrame()
的優點之下縮短非同步呼叫堆疊;之所以 setInterval()
要設得很短,是因為不然的話 requestAnimationFrame()
就形同虛設了。其基本結構大概是這樣:
function draw() {
// 執行動畫的繪製動作...
}
let request;
setInterval(() => {
cancelAnimationFrame(request);
request = requestAnimationFrame(draw);
}, 1);
這個作法並不算差,尤其實際上它並不會影響到 FPS(實測結果顯示至少在 Chrome 當中是如此)。但是我對這個解法只有一個小小的不滿在於,當我進行效能診斷的時候,會發現時間軸密密麻麻地全部都被我的 setInterval()
佔滿了:
作為比較,原本的作法的時間軸會是這樣:
看得出來差超多的。這些大量的 setInterval()
除了讓我的強迫症發作之外,也會干擾我診斷真正重要的事件觸發。
因此,我想了一個折衷的辦法:一方面利用 setInterval()
來避免非同步堆疊無止盡累積,但是另一方面又不要讓 setInterval()
太過頻繁地呼叫。其結構是這樣:
function draw() {
// 執行動畫的繪製動作...
request = requestAnimationFrame(draw); // 還是一樣自我呼叫
}
let request;
setInterval(() => {
cancelAnimationFrame(request);
request = requestAnimationFrame(draw);
}, 300); // 但每隔約 1/3 秒重設堆疊
在這樣的結構之下,視裝置效能而定、非同步呼叫堆疊頂多堆到 15 層左右,對偵錯的影響不大,而同時效能診斷的時間軸也變得幾乎跟上面第二張圖一樣稀疏,只有偶爾會穿插 setInterval()
的呼叫而已。以上是我最後找到最滿意的解決方案,給大家參考。
留言