requestAnimationFrame定時動畫屏幕刷新率節(jié)流示例淺析
前言
很長時間以來,計時器和定時執(zhí)行都是 JavaScript 動畫最先進(jìn)的工具。雖然 CSS 過渡和動畫方便了開發(fā)者實(shí)現(xiàn)某些動畫,但 JavaScript 動畫領(lǐng)域多年來進(jìn)展甚微。requestAnimationFrame() 方法應(yīng)運(yùn)而生,這個方法會告訴瀏覽器要執(zhí)行動畫了,于是瀏覽器可以通過最優(yōu)方式確定重繪的時序。
早期定時動畫
以前,在 JavaScript 中創(chuàng)建動畫基本上就是使用 setInterval()
來控制動畫的執(zhí)行:
(function () { function updateAnimations() { doAnimation1(); doAnimation2(); // ... } setInterval(updateAnimations, 100); })();
這種定時動畫的問題在于,無法準(zhǔn)確知曉循環(huán)之間的延時。
無論是 setInterval()
還是 setTimeout()
,都是不能保證時間精度的。作為第二個參數(shù)的延時只能保證何時會把代碼添加到瀏覽器的任務(wù)隊列,并不能保證添加到隊列就會立即執(zhí)行。如果隊列前面還有其他任務(wù),那么就要等這些任務(wù)執(zhí)行完再執(zhí)行。
簡單來講,這里的毫秒延時不是指何時這些代碼會執(zhí)行,而是指到時候會把回調(diào)添加到任務(wù)隊列。如果添加到隊列后,主線程還被其他任務(wù)占用,那么回調(diào)就不會立即執(zhí)行。
知道何時繪制下一幀是創(chuàng)造平滑動畫的關(guān)鍵,所以 setInterval()
和 setTimeout()
的不精確是個大問題。
瀏覽器自身計時器的精度讓這個問題雪上加霜。瀏覽器計時器的精度不足毫秒,最厲害的 Chrome 計時器精度為 4ms。更麻煩的是,瀏覽器又開始對切換到后臺或不活躍的標(biāo)簽頁中的計時器執(zhí)行限流,因此即使將時間間隔設(shè)為最優(yōu),也免不了只能得到近似的結(jié)果。
屏幕刷新率
一般計算機(jī)顯示器的屏幕刷新率都是 60HZ,基本上意味著每秒需要重繪 60 次。大多數(shù)瀏覽器會限制重繪頻率,使其不超過屏幕的刷新率,因?yàn)槌^屏幕刷新率用戶也感知不到。
所以,實(shí)現(xiàn)平滑動畫最佳的重繪時間間隔為 1000ms/60,大約 17 ms。以這個速度重繪可以實(shí)現(xiàn)最平滑的動畫,因?yàn)檫@已經(jīng)是瀏覽器的極限了。
requestAnimationFrame
requestAnimationFrame()
這個方法可以通知瀏覽器某些 JavaScript 代碼要執(zhí)行動畫了,這樣瀏覽器就可以在運(yùn)行某些代碼后進(jìn)行適當(dāng)?shù)膬?yōu)化。
requestAnimationFrame()
這個方法接收一個參數(shù),該參數(shù)是一個要在重繪屏幕前調(diào)用的函數(shù)。這個函數(shù)就是修改 DOM 樣式以反映下一次重繪有什么變化的地方。為了實(shí)現(xiàn)動畫循環(huán),可以把多個 requestAnimationFrame()
調(diào)用串聯(lián)起來,就像以前使用 setTimeout()
一樣:
function updateProgress() { var div = document.getElementById("status"); div.style.width = parseInt(div.style.width, 10) + 5 + "%"; if (div.style.left != "100%") { requestAnimationFrame(updateProgress); } } requestAnimationFrame(updateProgress);
因?yàn)?requestAnimationFrame()
只會調(diào)用一次傳入的函數(shù),所以每次更新用戶界面時需要再手動調(diào)用它一次。同時,也需要控制動畫何時停止。結(jié)果就會得到非常平滑的動畫。
requestAnimationFrame()
已經(jīng)解決了瀏覽器不知道 JavaScript 動畫何時開始的問題,以及最佳間隔時間是多少的問題。但是,如果我們想知道自己的代碼實(shí)際的執(zhí)行時間呢?同樣有解決方案。
傳給 requestAnimationFrame()
的函數(shù)實(shí)際上可以接收一個參數(shù),該參數(shù)表示下次重繪的時間。這一點(diǎn)非常重要:requestAnimationFrame()
實(shí)際上把重繪任務(wù)安排在了未來一個已知的時間點(diǎn)上,而且通過這個參數(shù)告訴了開發(fā)者,那么基于這個參數(shù),就可以更好地決定如何調(diào)優(yōu)動畫了:
function foo(t) { console.log(t); requestAnimationFrame(foo); } requestAnimationFrame(foo);
cancelAnimationFrame
const requestID = window.requestAnimationFrame((t) => { console.log(t); }); window.cancelAnimationFrame(requestID);
通過 requestAnimationFrame 節(jié)流
支持這個方法的瀏覽器實(shí)際上會暴露出作為鉤子的回調(diào)隊列。所謂鉤子,就是瀏覽器在執(zhí)行下一次重繪之前的一個點(diǎn)。這個回調(diào)隊列是一個可修改的函數(shù)列表,包含應(yīng)該在重繪之前調(diào)用的函數(shù)。每次調(diào)用 requestAnimationFrame()
都會在隊列上推入一個回調(diào)函數(shù),隊列的長度沒有限制。
這個回調(diào)隊列的行為不一定跟動畫有關(guān)。通過 requestAnimationFrame()
遞歸地向隊列中加入回調(diào)函數(shù),可以保證每次重繪最多只調(diào)用一次回調(diào)函數(shù),這是一個非常好的節(jié)流工具。在頻繁執(zhí)行影響頁面外觀的代碼時(比如滾動事件監(jiān)聽器),可以利用這個回調(diào)隊列進(jìn)行節(jié)流。
先看一個原生實(shí)現(xiàn),其中的滾動事件監(jiān)聽器每次觸發(fā)都會調(diào)用名為 expensiveOperation()
(耗時操作) 的函數(shù)。當(dāng)向下滾動網(wǎng)頁時,這個事件很快就會被觸發(fā)并執(zhí)行成百上千次:
function expensiveOperation() { console.log("Invoked at", Date.now()); } window.addEventListener("scroll", () => { expensiveOperation(); });
如果想把事件處理程序的調(diào)用限制在每次重繪之前,那么就可以把它封裝到 requestAnimationFrame()
調(diào)用中:
function expensiveOperation() { console.log("Invoked at", Date.now()); } window.addEventListener("scroll", () => { window.requestAnimationFrame(expensiveOperation); });
這樣會把所有回調(diào)的執(zhí)行集中在重繪鉤子,但不會過濾掉每次重繪的多余調(diào)用。我們可以定義一個標(biāo)志變量,在回調(diào)中設(shè)置其狀態(tài),就能將多余的調(diào)用屏蔽:
let enqueued = false; function expensiveOperation() { console.log("Invoked at", Date.now()); enqueued = false; } window.addEventListener("scroll", () => { if (!enqueued) { enqueued = true; window.requestAnimationFrame(expensiveOperation); } });
因?yàn)橹乩L是非常頻繁的操作,所以這算不上是真正的節(jié)流。更好的方法是配合使用一個計時器來限制操作執(zhí)行的頻率。這樣,計時器可以限制實(shí)際的操作執(zhí)行間隔,而 requestAnimationFrame()
控制在瀏覽器的哪個渲染周期中執(zhí)行:
let enabled = true; function expensiveOperation() { console.log("Invoked at", Date.now()); } window.addEventListener("scroll", () => { if (enabled) { enqueued = false; window.requestAnimationFrame(expensiveOperation); window.setTimeout(() => (enabled = true), 50); } });
上面的例子將回調(diào)限制為大約 50ms 執(zhí)行一次。
以上就是requestAnimationFrame定時動畫屏幕刷新率節(jié)流示例淺析的詳細(xì)內(nèi)容,更多關(guān)于requestAnimationFrame刷新節(jié)流的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript數(shù)組對象高階函數(shù)reduce的妙用詳解
這篇文章主要為大家介紹了JavaScript數(shù)組對象高階函數(shù)reduce的妙用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04微信小程序中input標(biāo)簽詳解及簡單實(shí)例
這篇文章主要介紹了微信小程序中input標(biāo)簽詳解及簡單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-05-05JS屬性scrollTop?clientHeight?scrollHeight理解學(xué)習(xí)
這篇文章主要為大家介紹了JS屬性scrollTop?clientHeight?scrollHeight理解學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07Lodash加減乘除add?subtract?multiply?divide方法源碼解讀
這篇文章主要介紹了Lodash加減乘除add?subtract?multiply?divide方法源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05微信小程序 天氣預(yù)報開發(fā)實(shí)例代碼源碼
這篇文章主要介紹了微信小程序 天氣預(yù)報開發(fā)實(shí)例代碼源碼的相關(guān)資料,這里含有源碼,需要的朋友可以參考下2017-01-01直觀詳細(xì)的typescript隱式類型轉(zhuǎn)換圖文詳解
這篇文章主要為大家介紹了直觀詳細(xì)的typescript隱式類型轉(zhuǎn)換圖文詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07微信小程序Server端環(huán)境配置詳解(SSL, Nginx HTTPS,TLS 1.2 升級)
這篇文章主要介紹了微信小程序Server端環(huán)境配置詳解(SSL, Nginx HTTPS,TLS 1.2 升級)的相關(guān)資料,需要的朋友可以參考下2017-01-01TypeScript順時針打印矩陣實(shí)現(xiàn)實(shí)例詳解
這篇文章主要為大家介紹了TypeScript順時針打印矩陣實(shí)現(xiàn)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09