你知道的requestAnimationFrame【從0到0.1】

隨着技術與設備的發展,用戶的終端對動畫的表現能力愈來愈強,更多的場景開始大量使用動畫。在 Web 應用中,實現動畫效果的方法比較多,JavaScript 中能夠經過定時器 setTimeout 來實現,css3 可使用 transitionanimation 來實現,html5 中的 canvas 也能夠實現。除此以外,html5 還提供一個專門用於請求動畫的 API,即 requestAnimationFramejavascript

本文內容均非原創,而是在知識點的收集與搬運中學習與理解,也歡迎你們收集與搬運本篇文章!css

1. 是什麼

  • HTML5 新增長的 API,相似於 setTimeout 定時器html

  • window 對象的一個方法,window.requestAnimationFramehtml5

    partial interface Window {
      long requestAnimationFrame(FrameRequestCallback callback);
      void cancelAnimationFrame(long handle);
    };
    複製代碼
  • 瀏覽器(因此只能在瀏覽器中使用)專門爲動畫提供的 API,讓 DOM 動畫、Canvas 動畫、SVG 動畫、WebGL 動畫等有一個統一的刷新機制java

2. 作什麼

  • 瀏覽器重繪頻率通常會和顯示器的刷新率保持同步。大多數瀏覽器採起 W3C 規範的建議,瀏覽器的渲染頁面的標準幀率也爲 60FPS(frames/ per second)
  • 按幀對網頁進行重繪。該方法告訴瀏覽器但願執行動畫並請求瀏覽器在下一次重繪以前調用回調函數來更新動畫css3

  • 由系統來決定回調函數的執行時機,在運行時瀏覽器會自動優化方法的調用git

    • 顯示器有固定的刷新頻率(60Hz 或 75Hz),也就是說,每秒最多隻能重繪 60 次或 75 次,requestAnimationFrame 的基本思想讓頁面重繪的頻率與這個刷新頻率保持同步github

      好比顯示器屏幕刷新率爲 60Hz,使用requestAnimationFrame API,那麼回調函數就每1000ms / 60 ≈ 16.7ms執行一次;若是顯示器屏幕的刷新率爲 75Hz,那麼回調函數就每1000ms / 75 ≈ 13.3ms執行一次。web

    • 經過requestAnimationFrame調用回調函數引發的頁面重繪或迴流的時間間隔和顯示器的刷新時間間隔相同。因此 requestAnimationFrame 不須要像setTimeout那樣傳遞時間間隔,而是瀏覽器經過系統獲取並使用顯示器刷新頻率canvas

      好比一個動畫,寬度從 0px 加一遞增到 100px。無緩動效果的狀況下,瀏覽器重繪一次,寬度就加 1。

3. 用法

動畫幀請求回調函數列表:每一個 Document 都有一個動畫幀請求回調函數列表,該列表能夠當作是由<handle, callback>元組組成的集合。

  • handle 是一個整數,惟一地標識了元組在列表中的位置,cancelAnimationFrame()能夠經過它中止動畫
  • callback 是一個無返回值的、形參爲一個時間值的函數(該時間值爲由瀏覽器傳入的從 1970 年 1 月 1 日到當前所通過的毫秒數)。
  • 剛開始該列表爲空。

頁面可見性 API

  • 當頁面被最小化或者被切換成後臺標籤頁時,頁面爲不可見,瀏覽器會觸發一個visibilitychange事件,並設置document.hidden屬性爲true
  • 當頁面切換到顯示狀態,頁面變爲可見,同時觸發一個visibilitychange事件,設置document.hidden屬性爲false
  • 調用操做。與setTimeout類似,可是不須要設置間隔時間,使用一個回調函數做爲參數,返回一個大於 0 的整數

    handle = requestAnimationFrame(callback);
    複製代碼
    • 參數callback,是一個回調函數,在下次從新繪製動畫時調用。該回調函數接收惟一參數,是一個高精度時間戳(performance.now()),指觸發回調函數的當前時間(不用手動傳入)
    • 返回值是一個long型的非零整數,是requestAnimationFrame回調函數列表中惟一的標識,表示定時器的編號,無其餘意義
  • 取消操做

    cancelAnimationFrame(handle);
    複製代碼
    • 參數是調用requestAnimationFrame時的返回值
    • 取消操做沒有返回值
  • 瀏覽器執行過程

    • 首先判斷document.hidden屬性是否爲true(頁面是否可見),頁面處於可見狀態纔會執行後面步驟

    • 瀏覽器清空上一輪的動畫函數

    • requestAnimationFrame將回調函數追加到動畫幀請求回調函數列表的末尾

      當執行requestAnimationFrame(callback)的時候,不會當即調用 callback 函數,只是將其放入隊列。每一個回調函數都有一個布爾標識cancelled,該標識初始值爲false,而且對外不可見。

    • 當瀏覽器再執行列表中的回調函數的時候,判斷每一個元組的 callback 的cancelled,若是爲false,則執行 callback

      當頁面可見而且動畫幀請求回調函數列表不爲空,瀏覽器會按期將這些回調函數加入到瀏覽器 UI 線程的隊列中

    • 博客園上yyc 元超的文章深刻理解 requestAnimationFrame中提供了讓一個僞代碼,用來講明「採樣全部動畫」任務的執行步驟

      var list = {};
      var browsingContexts = 瀏覽器頂級上下文及其下屬的瀏覽器上下文;
      for (var browsingContext in browsingContexts) {
      /* !將時間值從 DOMTimeStamp 更改成 DOMHighResTimeStamp 是 W3C 針對基於腳本動畫計時控制規範的最新編輯草案中的最新更改, * 而且某些供應商仍將其做爲 DOMTimeStamp 實現。 * 較早版本的 W3C 規範使用 DOMTimeStamp,容許你將 Date.now 用於當前時間。 * 如上所述,某些瀏覽器供應商可能仍實現 DOMTimeStamp 參數,或者還沒有實現 window.performance.now 計時函數。 * 所以須要用戶進行polyfill */
          var time = DOMHighResTimeStamp   //從頁面導航開始時測量的高精確度時間。DOMHighResTimeStamp 以毫秒爲單位,精確到千分之一毫秒。此時間值不直接與 Date.now() 進行比較,後者測量自 1970 年 1 月 1 日至今以毫秒爲單位的時間。若是你但願將 time 參數與當前時間進行比較,請使用當前時間的 window.performance.now。
        var d = browsingContext 的 active document;   //即當前瀏覽器上下文中的Document節點
          //若是該active document可見
          if (d.hidden !== true) {
              //拷貝 active document 的動畫幀請求回調函數列表到 list 中,並清空該列表
              var doclist = d的動畫幀請求回調函數列表
              doclist.appendTo(list);
              clear(doclist);
          }
          //遍歷動畫幀請求回調函數列表的元組中的回調函數
          for (var callback in list) {
              if (callback.cancelled !== true) {
                  try {
                      //每一個 browsingContext 都有一個對應的 WindowProxy 對象,WindowProxy 對象會將 callback 指向 active document 關聯的 window 對象。
                      //傳入時間值time
                      callback.call(window, time);
                  }
                  //忽略異常
                  catch (e) {
                  }
              }
          }
      }
      複製代碼
    • 當調用cancelAnimationFrame(handle)時,瀏覽器會設置該 handle 指向的回調函數的cancelledtrue(不管該回調函數是否在動畫幀請求回調函數列表中)。若是該 handle 沒有指向任何回調函數,則什麼也不會發生。

  • 遞歸調用。要想實現一個完整的動畫,應該在回調函數中遞歸調用回調函數

    let count = 0;
    let rafId = null;
    /** * 回調函數 * @param time requestAnimationFrame 調用該函數時,自動傳入的一個時間 */
    function requestAnimation(time) {
      console.log(time);
      // 動畫沒有執行完,則遞歸渲染
      if (count < 50) {
        count++;
        // 渲染下一幀
        rafId = requestAnimationFrame(requestAnimation);
      }
    }
    // 渲染第一幀
    requestAnimationFrame(requestAnimation);
    複製代碼
  • 若是在執行回調函數或者 Document 的動畫幀請求回調函數列表被清空以前屢次調用 requestAnimationFrame 調用同一個回調函數,那麼列表中會有多個元組指向該回調函數(它們的 handle 不一樣,但 callback 都爲該回調函數),「採集全部動畫」任務會執行屢次該回調函數。(類比定時器setTimeout

    function counter() {
      let count = 0;
      function animate(time) {
        if (count < 50) {
          count++;
          console.log(count);
          requestAnimationFrame(animate);
        }
      }
      requestAnimationFrame(animate);
    }
    btn.addEventListener("click", counter, false);
    複製代碼
    • 屢次點擊按鈕,會發現打印出來多個序列數值(下圖中,連續觸發三次,打印了三個有序列)

      屢次調用回調函數

    • 若是是做用於動畫,動畫會出現突變的狀況

4. 兼容性

來源:Polyfill for requestAnimationFrame/cancelAnimationFrame

在瀏覽器初次加載的時候執行下面的代碼便可。

// 使用 Date.now 獲取時間戳性能比使用 new Date().getTime 更高效
if (!Date.now)
  Date.now = function() {
    return new Date().getTime();
  };

(function() {
 "use strict";

  var vendors = ["webkit", "moz"];
  for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
    var vp = vendors[i];
    window.requestAnimationFrame = window[vp + "RequestAnimationFrame"];
    window.cancelAnimationFrame =
      window[vp + "CancelAnimationFrame"] ||
      window[vp + "CancelRequestAnimationFrame"];
  }
  // 上面方法都不支持的狀況,以及IOS6的設備
  // 使用 setTimeout 模擬實現
  if (
    /iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) ||
    !window.requestAnimationFrame ||
    !window.cancelAnimationFrame
  ) {
    var lastTime = 0;
    // 和經過時間戳實現節流功能的函數類似
    window.requestAnimationFrame = function(callback) {
      var now = Date.now();
      var nextTime = Math.max(lastTime + 16, now);
      // 實際上第1幀是不許確的,首次nextTime - now = 0
      return setTimeout(function() {
        callback((lastTime = nextTime));
      }, nextTime - now);
    };
    window.cancelAnimationFrame = clearTimeout;
  }
})();
複製代碼

5. 優點

requestAnimationFrame採用系統時間間隔,保持最佳繪製效率。不會由於間隔時間太短,形成過分繪製,增長開銷;也不會由於間隔時間過長,使動畫卡頓。

從實現的功能和使用方法上,requestAnimationFrame與定時器setTimeout都類似,因此說其優點是同setTimeout實現的動畫相比。

a. 提高性能,防止掉幀

  • 瀏覽器 UI 線程:瀏覽器讓執行 JavaScript 和更新用戶界面(包括重繪和迴流)共用同一個單線程,稱爲「瀏覽器 UI 線程」
  • 瀏覽器 UI 線程的工做基於一個簡單的隊列系統,任務會被保存到隊列中直到進程空閒。一旦空閒,隊列中的下一個任務就被從新提取出來並運行。這些任務要麼是運行 JavaScript 代碼,要麼執行 UI 更新。
  • 經過setTimeout實現動畫

    • setTimeout經過設置一個間隔時間不斷改變圖像,達到動畫效果。該方法在一些低端機上會出現卡頓、抖動現象。這種現象通常有兩個緣由:

      • setTimeout的執行時間並非肯定的。

        在 JavaScript 中,setTimeout任務被放進異步隊列中,只有當主線程上的任務執行完之後,纔會去檢查該隊列的任務是否須要開始執行。因此,setTimeout的實際執行時間通常比其設定的時間晚一些。這種運行機制決定了時間間隔參數實際上只是指定了把動畫代碼添加到【瀏覽器 UI 線程隊列】中以等待執行的時間。若是隊列前面已經加入了其餘任務,那動畫代碼就要等前面的任務完成後再執行

        let startTime = performance.now();
        setTimeout(() => {
          let endTime = performance.now();
          console.log(endTime - startTime);
        }, 50);
        /* 一個很是耗時的任務 */
        for (let i = 0; i < 20000; i++) {
          console.log(0);
        }
        複製代碼

        定時器

      • 刷新頻率受屏幕分辨率和屏幕尺寸影響,不一樣設備的屏幕刷新率可能不一樣,setTimeout只能設置固定的時間間隔,這個時間和屏幕刷新間隔可能不一樣

    • 以上兩種狀況都會致使setTimeout的執行步調和屏幕的刷新步調不一致,從而引發丟幀現象。

      • setTimeout的執行只是在內存中對圖像屬性進行改變,這個改變必需要等到下次瀏覽器重繪時纔會被更新到屏幕上。若是和屏幕刷新步調不一致,就可能致使中間某些幀的操做被跨越過去,直接更新下下一幀的圖像。

        假如使用定時器設置間隔 10ms 執行一個幀,而瀏覽器刷新間隔是 16.6ms(即 60FPS)

        丟幀

        由圖可知,在 20ms 時,setTimeout調用回調函數在內存中將圖像的屬性進行了修改,可是此時瀏覽器下次刷新是在 33.2ms 的時候,因此 20ms 修改的圖像沒有更新到屏幕上。 而到了 30ms 的時候,setTimeout又一次調用回調函數並改變了內存中圖像的屬性,以後瀏覽器就刷新了,20ms 更新的狀態被 30ms 的圖像覆蓋了,屏幕上展現的是 30ms 時的圖像,因此 20ms 的這一幀就丟失了。丟失的幀多了,畫面就卡頓了。

  • 使用 requestAnimationFrame 執行動畫,最大優點是能保證回調函數在屏幕每一次刷新間隔中只被執行一次,這樣就不會引發丟幀,動畫也就不會卡頓

b. 節約資源,節省電源

  • 使用 setTimeout 實現的動畫,當頁面被隱藏或最小化時,定時器setTimeout仍在後臺執行動畫任務,此時刷新動畫是徹底沒有意義的(實際上 FireFox/Chrome 瀏覽器對定時器作了優化:頁面閒置時,若是時間間隔小於 1000ms,則中止定時器,與requestAnimationFrame行爲相似。若是時間間隔>=1000ms,定時器依然在後臺執行)

    // 在瀏覽器開發者工具的Console頁執行下面代碼。
    // 當開始輸出count後,切換瀏覽器tab頁,再切換回來,能夠發現打印的值沒有中止,甚至可能已經執行完了
    let count = 0;
    let timer = setInterval(() => {
      if (count < 20) {
        count++;
        console.log(count);
      } else {
        clearInterval(timer);
        timer = null;
      }
    }, 2000);
    複製代碼
  • 使用requestAnimationFrame,當頁面處於未激活的狀態下,該頁面的屏幕刷新任務會被系統暫停,因爲requestAnimationFrame保持和屏幕刷新同步執行,因此也會被暫停。當頁面被激活時,動畫從上次停留的地方繼續執行,節約 CPU 開銷。

    // 在瀏覽器開發者工具的Console頁執行下面代碼。
    // 當開始輸出count後,切換瀏覽器tab頁,再切換回來,能夠發現打印的值從離開前的值繼續輸出
    let count = 0;
    function requestAnimation() {
      if (count < 500) {
        count++;
        console.log(count);
        requestAnimationFrame(requestAnimation);
      }
    }
    requestAnimationFrame(requestAnimation);
    複製代碼

c. 函數節流

  • 一個刷新間隔內函數執行屢次時沒有意義的,由於顯示器每 16.7ms 刷新一次,屢次繪製並不會在屏幕上體現出來
  • 在高頻事件(resizescroll等)中,使用requestAnimationFrame能夠防止在一個刷新間隔內發生屢次函數執行,這樣保證了流暢性,也節省了函數執行的開銷
  • 某些狀況下能夠直接使用requestAnimationFrame替代 Throttle 函數,都是限制回調函數執行的頻率

6. 應用

  • 簡單的進度條動畫

    function loadingBar(ele) {
      // 使用閉包保存定時器的編號
      let handle;
      return () => {
        // 每次觸發將進度清空
        ele.style.width = "0";
        // 開始動畫前清除上一次的動畫定時器
        // 不然會開啓多個定時器
        cancelAnimationFrame(handle);
        // 回調函數
        let _progress = () => {
          let eleWidth = parseInt(ele.style.width);
          if (eleWidth < 200) {
            ele.style.width = `${eleWidth + 5}px`;
            handle = requestAnimationFrame(_progress);
          } else {
            cancelAnimationFrame(handle);
          }
        };
        handle = requestAnimationFrame(_progress);
      };
    }
    複製代碼
  • 添加緩動效果,實現一個元素塊按照三階貝塞爾曲線的ease-in-out緩動特效參數運動。如何使用 Javascript 實現緩動特效

緩動動畫:指定動畫效果在執行時的速度,使其看起來更加真實。

/** * @param {HTMLElement} ele 元素節點 * @param {number} change 改變量 * @param {number} duration 動畫持續時長 */
function moveBox(ele, change, duration) {
  // 使用閉包保存定時器標識
  let handle;
  // 返回動畫函數
  return () => {
    // 開始時間
    let startTime = performance.now();
    // 防止啓動多個定時器
    cancelAnimationFrame(handle);
    // 回調函數
    function _animation() {
      // 這一幀開始的時間
      let current = performance.now();
      let eleTop = ele.offsetLeft;
      // 這一幀內元素移動的距離
      let left = change * easeInOutCubic((current - startTime) / duration);
      ele.style.left = `${~~left}px`;
      // 判斷動畫是否執行完
      if ((current - startTime) / duration < 1) {
        handle = requestAnimationFrame(_animation);
      } else {
        cancelAnimationFrame(handle);
      }
    }
    // 第一幀開始
    handle = requestAnimationFrame(_animation);
  };
}
/** * 三階貝塞爾曲線ease-in-out * @param {number} k */
function easeInOutCubic(k) {
  return (k *= 2) < 1 ? 0.5 * k * k * k : 0.5 * ((k -= 2) * k * k + 2);
}
複製代碼

7. 相關

本文內容均非原創,而是在知識點的收集與搬運中學習與理解,也歡迎你們收集與搬運本篇文章!

  • https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
  • https://caniuse.com/#search=requestAnimationFrame
  • https://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-%E5%8A%A8%E7%94%BB%E7%AE%97%E6%B3%95/
  • https://javascript.ruanyifeng.com/htmlapi/requestanimationframe.html
  • https://www.cnblogs.com/xiaohuochai/p/5777186.html
  • https://juejin.im/post/5b6020b8e51d4535253b30d1
  • https://www.cnblogs.com/chaogex/p/3960175.html#explain
  • http://www.softwhy.com/article-7204-1.html
  • https://easings.net/zh-cn#
  • https://zhuanlan.zhihu.com/p/25676357
  • https://www.cnblogs.com/onepixel/p/7078617.html
相關文章
相關標籤/搜索