JavaScript >> Javascript 文檔 >  >> JavaScript

使用 requestAnimationFrame 更好的 JavaScript 動畫

很長一段時間以來,計時器和間隔一直是基於 JavaScript 動畫的最先進技術。雖然 CSS 過渡和動畫使一些動畫對 Web 開發人員來說很容易,但這些年來基於 JavaScript 的動畫世界幾乎沒有什麼變化。也就是說,直到 Firefox 4 發布了第一種改進 JavaScript 動畫的方法。但要充分了解改進,看看動畫在網絡上是如何演變的會有所幫助。

定時器

創建動畫的第一個模式是使用鍊式 setTimeout() 來電。長期的開發人員會記得在 Netscape 3 的鼎盛時期對狀態欄新聞代碼的痴迷。它通常看起來像這樣:

(function(){
    var msg = "NFL Draft is live tonight from Radio City Music Hall in New York City!"
        len = 25,
        pos = 0,
        padding = msg.replace(/./g, " ").substr(0,len)
        finalMsg = padding + msg;

    function updateText(){
        var curMsg = finalMsg.substr(pos++, len);
        window.status = curMsg;
        if (pos == finalMsg.length){
            pos = 0;
        }
        setTimeout(updateText, 100);
    }

    setTimeout(updateText, 100);

})();

如果您想在瀏覽器中測試此代碼,請創建一個 <pre> 元素並使用它代替 window.status ,就像我做這個新貼紙的例子一樣。

這種煩人的網絡模式後來被 window.status 的限制所抵消 ,但是隨著 Internet Explorer 4 和 Netscape 4 的發布,基本技術重新出現,這是第一個讓開發人員能夠更好地控制頁面上元素佈局的瀏覽器。隨之而來的是使用 JavaScript 動態更改元素的大小、位置、顏色等的能力,以及全新的動畫類型。例如。以下動畫 <div> 到 100% 的寬度(通常在進度條中找到):

(function(){

    function updateProgress(){
        var div = document.getElementById("status");
        div.style.width = (parseInt(div.style.width, 10) + 5) + "%";
        if (div.style.width != "100%"){
            setTimeout(updateProgress, 100);
        }

    }

    setTimeout(updateProgress, 100);

})();

儘管頁面的動畫部分不同,但基本技術保持不變:進行更改,使用 setTimeout() 產生並讓頁面更新,然後將調用計時器以應用下一個更改。重複此過程直到動畫完成(請參閱正在運行的進度條)。與早期狀態滾動條相同的技術,只是動畫不同。

鏈接到 setTimeout() 的調用 就像在這兩個示例中一樣,一起創建動畫循環。動畫循環在計算機程序中用於處理定期更新用戶界面。所有動畫循環都以相同的方式運行:更新、休眠、更新、休眠。早期,setTimeout() 是 JavaScript 的主要動畫循環技術。

間隔

隨著動畫成功地重新引入網絡(讓像我這樣的純粹主義者感到沮喪),新的探索出現了。只有一個動畫已經不夠好,必須有多個。第一次嘗試是創建多個動畫循環,每個動畫一個循環。使用 setTimeout() 創建多個計時器 事實證明這些早期瀏覽器處理起來有點多,因此開發人員開始使用單個動畫循環,使用 setInterval() 創建 , 管理頁面上的所有動畫。使用 setInterval() 的基本動畫循環 看起來像這樣:

(function(){

    function updateAnimations(){
        updateText();
        updateProgress();
    }

    setInterval(updateAnimations, 100);

})();

要構建一個小型動畫庫,updateAnimations() 方法將循環播放正在運行的動畫並對每個動畫進行適當的更改(請參閱新聞代碼和一起運行的進度條)。如果沒有要更新的動畫,該方法可以不做任何事情而退出,甚至可能停止動畫循環,直到有更多動畫準備好更新。

這個動畫循環的棘手部分是知道延遲應該是多少。間隔必須足夠短,才能流暢地處理各種不同的動畫類型,但又要足夠長,以便產生瀏覽器實際可以渲染的變化。大多數計算機顯示器以 60 Hz 的速度刷新,這基本上意味著每秒重繪 60 次。大多數瀏覽器都會限制重繪,因此它們不會嘗試更頻繁地重繪,因為他們知道最終用戶的體驗不會得到改善。

鑑於此,最流暢動畫的最佳間隔是 1000ms / 60,或大約 17ms。以這種速度,您將看到最流暢的動畫,因為您更接近地反映了瀏覽器的功能。將此示例與前一個示例的間隔時間為 17 毫秒進行比較,您會看到更流暢的動畫(也更快,因為動畫更新更頻繁,我沒有進行任何計算以使其生效)。使用間隔為 17 毫秒的動畫循環時,可能需要限制多個動畫,以免完成太快。

問題

即使 setInterval() 基於 -based 的動畫循環比擁有多組 setTimeout() 更有效 基於循環,仍然存在問題。 setInterval() 都不是 也不是 setTimeout() 旨在準確。您指定為第二個參數的延遲僅表示何時將代碼添加到瀏覽器的 UI 線程隊列中以便可能執行。如果前面的隊列中有其他作業,則該代碼等待執行。簡而言之:毫秒延遲並不表示代碼何時將被執行 ,僅指示作業何時排隊 .如果 UI 線程很忙,可能正在處理用戶操作,那麼該代碼將不會立即執行。

了解何時繪製下一幀是平滑動畫的關鍵,直到最近,還沒有辦法保證下一幀何時會在瀏覽器中繪製。作為 <canvas> 隨著新的基於瀏覽器的遊戲開始流行,開發人員對 setInterval() 的不准確性越來越感到沮喪 和 setTimeout() .

加劇這些問題的是瀏覽器的計時器分辨率。計時器不精確到毫秒。以下是一些常見的計時器分辨率 1

  • Internet Explorer 8 及更早版本的計時器分辨率為 15.625 毫秒
  • Internet Explorer 9 及更高版本的計時器分辨率為 4 毫秒。
  • Firefox 和 Safari 的計時器分辨率約為 10 毫秒。
  • Chrome 的計時器分辨率為 4 毫秒。

版本 9 之前的 Internet Explorer 的計時器分辨率為 15.625 毫秒 1 ,因此 0 到 15 之間的任何值都可以是 0 或 15,但僅此而已。 Internet Explorer 9 將計時器分辨率提高到 4 毫秒,但在動畫方面仍然不是很具體。 Chrome 的計時器分辨率是 4 毫秒,而 Firefox 和 Safari 是 10 毫秒。因此,即使您將間隔設置為最佳顯示,您仍然只能接近您想要的時間。

mozRequestAnimationFrame

Mozilla 的 Robert O'Callahan 正在考慮這個問題並想出了一個獨特的解決方案。他指出,CSS 過渡和動畫受益於瀏覽器知道應該發生一些動畫,從而計算出刷新 UI 的正確間隔。使用 JavaScript 動畫,瀏覽器不知道動畫正在發生。他的解決方案是創建一個新方法,稱為 mozRequestAnimationFrame() ,這向瀏覽器表明某些 JavaScript 代碼正在執行動畫。這允許瀏覽器在運行一些代碼後進行適當的優化。

mozRequestAnimationFrame() 方法接受一個參數,這是一個調用 *prior * 重新繪製屏幕的函數。這個函數是你對 DOM 樣式進行適當更改的地方,這些更改將在下一次重繪時反映出來。為了創建動畫循環,您可以將多個調用鏈接到 mozRequestAnimationFrame() 以與之前使用 setTimeout() 相同的方式組合在一起 .示例:

function updateProgress(){

    var div = document.getElementById("status");
    div.style.width = (parseInt(div.style.width, 10) + 5) + "%";

    if (div.style.left != "100%"){
        mozRequestAnimationFrame(updateProgress);
    }
}

mozRequestAnimationFrame(updateProgress);

mozRequestAnimationFrame() 只運行一次給定的函數,您需要在下次要對動畫進行 UI 更改時再次手動調用它。您還需要以相同的方式管理何時停止動畫。很酷,結果是一個非常流暢的動畫,如這個增強的示例所示。

到目前為止,mozRequestAnimationFrame() 已經解決了瀏覽器不知道 JavaScript 動畫何時發生的問題以及不知道最佳間隔的問題,但是不知道代碼何時實際執行的問題呢?相同的解決方案也涵蓋了這一點。

您傳遞給 mozRequestAnimationFrame() 的函數 實際上接收一個參數,它是一個時間碼(自 1970 年 1 月 1 日以來的毫秒數),用於指示下一次重繪實際發生的時間。這是很重要的一點:mozRequestAnimationFrame() 實際上會為將來的某個已知點安排重繪,並可以告訴您何時重繪。然後,您可以確定如何最好地調整動畫。

為了確定自上次重繪以來已經過去了多少時間,您可以查詢 mozAnimationStartTime ,其中包含上次重繪的時間碼。從傳遞給回調的時間中減去這個值可以讓你準確地計算出在你的下一組更改被繪製到屏幕之前已經過去了多少時間。使用這些值的典型模式如下:

function draw(timestamp){

    //calculate difference since last repaint
    var diff = timestamp - startTime;

    //use diff to determine correct next step

    //reset startTime to this repaint
    startTime = timestamp;

    //draw again
    mozRequestAnimationFrame(draw);
}

var startTime = mozAnimationStartTime;
mozRequestAnimationFrame(draw);

關鍵是第一次調用 mozAnimationStartTime 在傳遞給 mozRequestAnimationFrame() 的回調之外 .如果你調用 mozAnimationStartTime 在回調內部,它將等於作為參數傳入的時間碼。

webkitRequestAnimationFrame

Chrome 的人們顯然對這種方法很興奮,因此創建了自己的實現,稱為 webkitRequestAnimationFrame() .此版本與 Firefox 版本在兩個方面略有不同。首先,它沒有將時間碼傳遞給回調函數,你不知道下一次重繪何時發生。其次,它添加了第二個可選參數,即發生更改的 DOM 元素。因此,如果您知道重繪只會發生在頁面上的一個特定元素內部,則可以將重繪限制在該區域。

沒有等效的 mozAnimationStartTime 也就不足為奇了 ,因為沒有下一次繪畫時間的信息不是很有用。但是,有一個 webkitCancelAnimationFrame() ,這會取消之前安排的重繪。

如果您不需要精確的時差,您可以使用以下模式為 Firefox 4+ 和 Chrome 10+ 創建動畫循環:

(function(){

    function draw(timestamp){

        //calculate difference since last repaint
        var drawStart = (timestamp || Date.now()),
            diff = drawStart - startTime;

        //use diff to determine correct next step

        //reset startTime to this repaint
        startTime = drawStart;

        //draw again
        requestAnimationFrame(draw);
    }

    var requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame,
        startTime = window.mozAnimationStartTime || Date.now();
    requestAnimationFrame(draw);

})();

此模式使用可用功能來創建動畫循環,並了解已經過去了多少時間。在 Firefox 中,這使用了可用的時間碼信息,而 Chrome 默認使用不太準確的 Date 對象。使用此模式時,時間差可以讓您大致了解已經過去了多少時間,但肯定不會告訴您下次在 Chrome 中發生重繪的時間。不過,最好知道已經過了多少時間,而不是什麼都沒有。

總結

mozRequestAnimationFrame()的介紹 方法可能是 Web 歷史上對改進 JavaScript 動畫最重要的貢獻。如前所述,自 JavaScript 早期以來,JavaScript 動畫的狀態幾乎相同。隨著瀏覽器在動畫方面的進步以及 CSS 過渡和動畫的引入,很高興看到基於 JavaScript 的動畫受到了一些關注,因為隨著 的普及,這些肯定會變得更加重要和 CPU 密集型 -基於遊戲。了解 JavaScript 何時嘗試動畫可以讓瀏覽器進行更優化的處理,包括在標籤處於後台或移動設備上的電池電量不足時停止處理。

requestAnimationFrame() API 現在被 W3C 起草為一項新的建議,並由 Mozilla 和 Google 作為 Web 性能組的一部分聯合工作。很高興看到這兩個團隊如此迅速地將兼容的(如果不是完全的)實現推向市場。

更新(2011 年 5 月 3 日) :修正了錯字,添加了移動信息。** **

更新(2011 年 5 月 4 日) :修復了增強示例的鏈接。

參考

  1. Chrome:調速時鐘,作者:Mike Belshe
  2. requestAnimationFrame 實現 (Chrome)

Tutorial JavaScript 教程
  1. 使用 Vanilla JavaScript 構建 Pokedex

  2. 將 HTML 字符串附加到 DOM

  3. 如何使用 Node.js 和 MongoDB 構建 URL 縮短器

  4. 用 Redwood 製作媒體庫

  5. 反應下拉組件

  6. This.JavaScript:框架狀態 - Vue 更新

  7. 15 個自定義 Hooks 讓你的 React 組件輕量級

  1. 用 JavaScript 編寫 VS Code 擴展,而不是 TypeScript ^

  2. JavaScript 中的接縫

  3. 為什麼要關心瀏覽器在 React 中的工作方式

  4. 使用 Cinnamon.js 通過同義詞查找單詞

  5. setTimeout 不是 JavaScipt 的一部分,為什麼?

  6. Handlebars 指南:Node/JavaScript 的模板引擎

  7. 我應該使用哪個服務提供商來託管我的生產後端?

  1. 使用 Google 表格進行抽獎活動

  2. Next Right Now in 2020 - Next.js 最靈活和生產就緒的樣板

  3. 如何在 React 中創建帶氣泡的範圍滑塊

  4. 使用 Node 自動化您的工作流程