關(guān)于JavaScript實現(xiàn)動畫時動畫抖動的原因與解決方法
前段時間在使用 JavaScript 做動畫的時候發(fā)現(xiàn)做出來的動畫會出現(xiàn)卡頓的現(xiàn)象,今天我們主要就來聊一下卡頓的原因以及如何解決這個問題。
使用定時器實現(xiàn)動畫出現(xiàn)卡頓的原因
- 主要原因是瀏覽器無法確定定時器的回調(diào)函數(shù)的執(zhí)行時機。以
setInterval
為例,當一個setInterval
定時器被創(chuàng)建后,它的回調(diào)任務(wù)會被放到異步隊列,只有當同步任務(wù)執(zhí)行完成后,瀏覽器才會檢查異步隊列中是否有需要執(zhí)行的異步任務(wù),如果有,就取出執(zhí)行,這樣會使任務(wù)的實際執(zhí)行時機比所設(shè)定的延遲時間要晚一些。
這個問題跟瀏覽器的事件循環(huán)機制有關(guān),JavaScript 引擎在解析執(zhí)行我們的代碼的時候,遇到定時器,會調(diào)用瀏覽器 API,讓定時器去進行倒計時,此時并不阻塞同步代碼的執(zhí)行,當定時器倒計時完畢,定時器回調(diào)會被放入宏任務(wù)隊列等待執(zhí)行。
在這個過程中問題就來了,如果說同步代碼的執(zhí)行需要50ms,而定時器設(shè)置的定時只有20ms,那么由于事件循環(huán)的機制,還是要等待同步任務(wù)執(zhí)行完整之后再來執(zhí)行微任務(wù)隊列中的定時器回調(diào),而這中間,又相隔了30ms,在這30ms的過程中,定時器的回調(diào)一直處于 pendding 的狀態(tài)。如果定時器中是動畫相關(guān)的操作,那也需要在預(yù)期的時間上多等待50ms。
畫了張圖,希望能幫助大家理解(如果不能幫助大家理解,那么請忽略這張圖……)
- 屏幕分辨率和尺寸也會影響刷新頻率,不同設(shè)備的屏幕繪制頻率可能會有所不同,而
setInterval
只能設(shè)置一個固定的時間間隔,這個間隔時間不一定與屏幕的刷新時間同步,所以就可能會導致動畫出現(xiàn)隨機丟幀的問題。
這里有兩個點,一個是顯示器的刷新頻率,另一個是定時器的時間間隔。
一般顯示器刷新頻率都是60Hz,這基本上意味著每秒需要重繪60次。大多數(shù)瀏覽器都會限制重繪的頻率,使其不超過顯示器的刷新頻率。因為超過刷新頻率,用戶也感知不到,白白浪費性能。
因此,實現(xiàn)平滑動畫最佳的重繪間隔為1000ms/60,大約17毫秒。以這個速度重繪,可以實現(xiàn)最平滑的動畫效果。因為這已經(jīng)是瀏覽器的重繪頻率的極限了。
知道何時繪制下一幀,是創(chuàng)造平滑動畫的關(guān)鍵。直到幾年前,都沒有確切保證讓瀏覽器在何時把下一幀繪制出來的方法。隨著 <canvas>
和 HTML5
游戲的興起,開發(fā)者發(fā)現(xiàn) setInterval
和 setTimeout
的不精確是個大問題,而瀏覽器自身的計時器也存在著精度不足毫秒的問題。
以下是幾個瀏覽器計時器的精度情況:
- IE8 以及之前的版本計時器精度為 15.625ms;
- IE9 及之后的版計時器精度為 4ms;
- FireFox 和 Safari 的計時器精度約為 10ms;
- Chrome 的計時器精度為 4ms。
以 Chrome 為例,它的計時器精度為 4ms,這意味著 0~4 之間的任何值最終要么是 0,要么是4;不可能是別的值。因此,即使將瀏覽器的時間間隔設(shè)置為最優(yōu),也免不了只能得到相近似的結(jié)果。
對于 JavaScript 來說,它不知道瀏覽器會在何時發(fā)生重繪。因此,我們通過定時器做動畫的時候,在定時器中控制動畫的代碼已經(jīng)執(zhí)行完成的情況下,動畫效果并不一定會立馬生效,因為此時瀏覽器可能還處在等待下一次重繪的過程中,當下一次重繪完成,動畫效果才能在瀏覽器窗口中顯示出來。
而由于瀏覽器計時器時間差的問題,會導致定時器的計時并不一定是我們設(shè)置的 17 ms,而是在多個時間點內(nèi)反復(fù)橫跳,也因此才出現(xiàn)使用定時器做動畫的時候動畫抖動的問題,在復(fù)雜的動畫中,這種問題尤為明顯。
在這樣的環(huán)境下,今天的主角 requestAnimationFrame
應(yīng)運而生!
requestAnimationFrame 的前世今生
Mozilla 的 Robert O'Callahan 一直在思考這個問題,并且提出了一個獨特的解決方案。他指出,瀏覽器知道 CSS 過渡和動畫應(yīng)該什么時候開始,并且能夠計算出正確的時間間隔,到時間就去刷新用戶界面。
但是對于 JavaScript 而言,瀏覽器并不知道動畫什么時候開始。他給出的方案是創(chuàng)造一個名為 MozRequestAnimationFrame
的新方法,以此來通知瀏覽器某些 JavaScript 代碼要執(zhí)行動畫了。這樣瀏覽器就可以在運行某些代碼后進行適當?shù)膬?yōu)化。
目前,所有的瀏覽器都支持這個方法不帶前綴的版本,也就是現(xiàn)在用到的 requestAnimationFrame
。
requestAnimationFrame VS setInterval
這里就不再過多的介紹 requestAnimationFrame
的詳細用法了,它的用法并不復(fù)雜。
與定時器不同的是,requestAnimationFrame
只會在被調(diào)用的時候執(zhí)行一次動畫,而不會連續(xù)執(zhí)行。如果想做連續(xù)的動畫,則可以通過遞歸來實現(xiàn)對 requestAnimationFrame
的連續(xù)調(diào)用。
接下來通過一個 demo 來對比一下使用 requestAnimationFrame
和 setInterval
兩者做出來的動畫效果之間的差異。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <style> .square1, .square2 { position: absolute; width: 100px; height: 100px; } .square1 { top: 40px; background: red; } .square2 { top: 150px; background: blue; } </style> <body> <div class="container"> <div class="square1"></div> <div class="square2"></div> <button class="btn">開始!</button> </div> <script> const square1El = document.querySelector('.square1') const square2El = document.querySelector('.square2') // 定時器版 function squareMove() { let timer = null square1El.style.left = '0px' timer = setInterval(() => { const squareLeft = parseInt(square1El.style.left) if (squareLeft >= 500) return clearInterval(timer) square1El.style.left = squareLeft + 1 + 'px' }, 17) } // requestAnimationFrame 版 function squareMove2() { let timer = null square2El.style.left = '0px' function updateAnimation() { const squareLeft = parseInt(square2El.style.left) if (squareLeft >= 500) return cancelAnimationFrame(timer) square2El.style.left = squareLeft + 1 + 'px' window.requestAnimationFrame(updateAnimation) } window.requestAnimationFrame(updateAnimation) } document.querySelector('.btn').addEventListener('click', () => { squareMove() squareMove2() }) </script> </body> </html>
在頁面中畫了兩個正方形,當點擊按鈕的時候方塊開始運動,紅色方塊是使用 setInterval
實現(xiàn)的動畫,藍色方塊使用的是 requestAnimationFrame
。
接下來看一下實現(xiàn)的效果。
在生成gif的時候視頻被壓縮了,但是還是能看到紅色的方塊在開始運動的時候有明顯的抖動,而藍色的方塊則比較絲滑。
實際上,requestAnimationFrame
的回調(diào)函數(shù)可以接收一個參數(shù),這個參數(shù)是一個 DOMHightResTimeStamp
的實例(比如:performance.now()的返回值),用來表示下一次重繪的時間。這一點非常重要,requestAnimationFrame
實際上是把重繪任務(wù)安排在了未來的一個已知的時間點上,而且通過這個參數(shù)來告訴開發(fā)者。
類似于 setInterval
的清除方法 clearInterval
,requestAnimationFrame
也有對應(yīng)的取消重繪的方法 cancelAnimationFrame
,用法也跟 clearInterval
非常類似,在每次調(diào)用 requestAnimationFrame
的時候,都會返回一個id,cancelAnimationFrame
就是通過這個id去取消對應(yīng)的 requestAnimationFrame
。
看到這里,大家應(yīng)該對 setInterval
和 requestAnimationFrame
都有了更深的了解,以后使用 JavaScript 做動畫還是以 requestAnimationFrame
為主。
希望講解的內(nèi)容能對大家有所幫助~
參考資料
[1]《JavaScript高級程序設(shè)計(第四版)》第18章第1節(jié)。
總結(jié)
到此這篇關(guān)于JavaScript實現(xiàn)動畫時動畫抖動的原因與解決方法的文章就介紹到這了,更多相關(guān)JS動畫抖動內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
頁面中body onload 和 window.onload 沖突的問題的解決
關(guān)于頁面中body onload 和 window.onload 沖突的問題的解決2009-07-07詳解ES6 中的Object.assign()的用法實例代碼
這篇文章主要介紹了ES6 Object.assign()的用法及用途,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01用javascript實現(xiàn)的電信鐵通(網(wǎng)通)自動跳轉(zhuǎn)源代碼
用javascript實現(xiàn)的電信鐵通(網(wǎng)通)自動跳轉(zhuǎn)源代碼...2007-11-11