學習使用API以可預測的方式執行動畫並安排事件
requestAnimationFrame()
是一個相對較新的瀏覽器API。它提供一種更可預測的方式來連接到瀏覽器的渲染循環。
目前,所有現代瀏覽器(包括IE 10+)都支援這個API。
它不是專門用於動畫的API,但這是它被最廣泛使用的領域。
JavaScript有一個事件循環(event loop)。它不斷運行以執行JavaScript。
在過去,使用setTimeout()
或setInterval()
執行動畫。你執行一點點動畫,然後使用setTimeout()
在幾毫秒後重複執行這段代碼:
const performAnimation = () => {
//...
setTimeout(performAnimation, 1000 / 60)
}
setTimeout(performAnimation, 1000 / 60)
或是
const performAnimation = () => {
//...
}
setInterval(performAnimation, 1000 / 60)
你可以透過獲取timeout或interval的參考並將其清除來停止動畫:
let timer
const performAnimation = () => {
//...
timer = setTimeout(performAnimation, 1000 / 60)
}
timer = setTimeout(performAnimation, 1000 / 60)
//...
clearTimeout(timer)
performAnimation()
調用之間的1000 / 60
間隔取決於顯示器的刷新頻率,大多數情況下是60 Hz(每秒重繪60次),因為由於其限制,如果顯示器無法顯示,執行重繪將是無用的。這導致我們有大約16.6毫秒的時間來顯示每一個幀。
這種方法的問題是,即使我們精確指定了這個間隔,瀏覽器可能忙於執行其他操作,我們的setTimeout調用可能不能及時進行重繪,並且將被延遲到下一個循環。
這很糟糕,因為我們丟失了一個幀,在下一個幀中,動畫將被執行兩次,使眼睛注意到不平滑的動畫。
在Glitch上的這個使用setTimeout()構建的動畫的示例中可以看到這個問題。
requestAnimationFrame
是執行動畫的標準方式,儘管代碼看起來非常類似於setTimeout/setInterval的代碼,它的工作方式非常不同:
let request
const performAnimation = () => {
request = requestAnimationFrame(performAnimation)
//執行動畫
}
requestAnimationFrame(performAnimation)
//...
cancelAnimationFrame(request) //停止動畫
在Glitch上的這個使用requestAnimationFrame()構建的動畫的示例展示了這個方法。
優化
自從引入requestAnimationFrame()
以來,它對CPU友好,如果當前窗口或選項卡不可見,動畫會停止。
在引入requestAnimationFrame()
之前,setTimeout/setInterval即使在選項卡隱藏時也運行,但現在,由於這種方法被證明對省電也是成功的,瀏覽器也為這些事件實現了節流,每秒最多允許1次執行。
使用requestAnimationFrame
,瀏覽器可以進一步優化資源消耗,使動畫更加流暢。
時間軸示例
這是如果使用setTimeout或setInterval的完美時間軸:
你擁有一組繪圖(綠色)和渲染(紫色)事件,你的代碼在黃色框中 - 順便提一下,這些顏色在瀏覽器開發工具中也被用來表示時間軸:
這個插圖展示了完美的情況。你每60毫秒進行繪圖和渲染,而你的動畫剛好介於其中,完全規律。
如果你對你的動畫函數使用更高的頻率調用:
請注意,在每個幀中,我們呼叫了4個動畫步驟,然後才進行渲染,這將使動畫感覺很不流暢。
如果setTimeout由於其他代碼阻塞事件循環而無法按時運行會怎樣?我們會錯過一個幀:
如果動畫步驟花費的時間比您預期的時間多一點會怎樣?
渲染和繪圖事件也會被延遲。
這是requestAnimationFrame()
在視覺上的運行方式:
所有的動畫程式碼在渲染和繪圖事件之前運行。這使得代碼更可預測,並且我們有很多時間來執行動畫,而不需要擔心超過我們有在16毫秒的間隔內執行的時間。
請查看Jake Archibald的這個很棒的關於該主題的視頻。