深刻理解requestAnimationFrame

原文  http://www.cnblogs.com/chaogex/p/3960175.html

前言

本文主要參考w3c資料,從底層實現原理的角度介紹了requestAnimationFrame、cancelAnimationFrame,給出了相關的示例代碼以及我對實現原理的理解和討論。javascript

本文介紹

瀏覽器中動畫有兩種實現形式:經過申明元素實現(如SVG中的php

元素)和腳本實現。html

能夠經過setTimeout和setInterval方法來在腳本中實現動畫,可是這樣效果可能不夠流暢,且會佔用額外的資源。可參考《Html5 Canvas核心技術》中的論述:html5

 

 

它們有以下的特徵:java

一、即便向其傳遞毫秒爲單位的參數,它們也不能達到ms的準確性。這是由於javascript是單線程的,可能會發生阻塞。web

二、沒有對調用動畫的循環機制進行優化。api

三、沒有考慮到繪製動畫的最佳時機,只是一味地以某個大體的事件間隔來調用循環。瀏覽器

其實,使用setInterval或setTimeout來實現主循環,根本錯誤就在於它們抽象等級不符合要求。咱們想讓瀏覽器執行的是一套能夠控制各類細節的api,實現如「最優幀速率」、「選擇繪製下一幀的最佳時機」等功能。可是若是使用它們的話,這些具體的細節就必須由開發者本身來完成。app

 

requestAnimationFrame不須要使用者指定循環間隔時間,瀏覽器會基於當前頁面是否可見、CPU的負荷狀況等來自行決定最佳的幀速率,從而更合理地使用CPU。dom

本文主要內容

  • 名詞說明
  • API接口
  • 處理模型
  • 已解決的問題
  • 注意事項
  • 參考資料

名詞說明

  • 動畫幀請求回調函數列表

每一個Document都有一個動畫幀請求回調函數列表,該列表能夠當作是由< handle, callback>元組組成的集合。其中handle是一個整數,惟一地標識了元組在列表中的位置;callback是一個無返回值的、形參爲一個時間值的函數(該時間值爲由瀏覽器傳入的從1970年1月1日到當前所通過的毫秒數)。 剛開始該列表爲空。

  • Document

Dom模型中定義的Document節點。

  • Active document

瀏覽器上下文browsingContext中的Document被指定爲active document。

  • browsingContext

    瀏覽器上下文。

瀏覽器上下文是呈現document對象給用戶的環境。 瀏覽器中的1個tab或一個窗口包含一個頂級瀏覽器上下文,若是該頁面有iframe,則iframe中也會有本身的瀏覽器上下文,稱爲嵌套的瀏覽器上下文。

  • DOM模型

詳見個人理解DOM。

  • document對象

當html文檔加載完成後,瀏覽器會建立一個document對象。它對應於Document節點,實現了HTML的Document接口。 經過該對象可得到整個html文檔的信息,從而對HTML頁面中的全部元素進行訪問和操做。

  • HTML的Document接口

該接口對DOM定義的Document接口進行了擴展,定義了 HTML 專用的屬性和方法。

詳見 The Document object

  • 頁面可見

當頁面被最小化或者被切換成後臺標籤頁時,頁面爲不可見,瀏覽器會觸發一個 visibilitychange事件,並設置document.hidden屬性爲true;切換到顯示狀態時,頁面爲可見,也一樣觸發一個 visibilitychange事件,設置document.hidden屬性爲false。

詳見 Page Visibility 、 Page Visibility(頁面可見性) API介紹、微拓展

  • 隊列

瀏覽器讓一個單線程共用於執行javascrip和更新用戶界面。這個線程一般被稱爲「瀏覽器UI線程」。 瀏覽器UI線程的工做基於一個簡單的隊列系統,任務會被保存到隊列中直到進程空閒。一旦空閒,隊列中的下一個任務就被從新提取出來並運行。這些任務要麼是運行javascript代碼,要麼執行UI更新,包括重繪和重排。

API接口

Window對象定義瞭如下兩個接口:

partial interface Window { long requestAnimationFrame(FrameRequestCallback callback); void cancelAnimationFrame(long handle); };

requestAnimationFrame

requestAnimationFrame方法用於通知瀏覽器重採樣動畫。

當requestAnimationFrame(callback)被調用時不會執行callback,而是會將元組< handle,callback>插入到動畫幀請求回調函數列表末尾(其中元組的callback就是傳入requestAnimationFrame的回調函數),而且返回handle值,該值爲瀏覽器定義的、大於0的整數,惟一標識了該回調函數在列表中位置。

每一個回調函數都有一個布爾標識cancelled,該標識初始值爲false,而且對外不可見。

在後面的「處理模型」 中咱們會看到,瀏覽器在執行「採樣全部動畫」的任務時會遍歷動畫幀請求回調函數列表,判斷每一個元組的callback的cancelled,若是爲false,則執行callback。

cancelAnimationFrame

cancelAnimationFrame 方法用於取消先前安排的一個動畫幀更新的請求。

當調用cancelAnimationFrame(handle)時,瀏覽器會設置該handle指向的回調函數的cancelled爲true。

不管該回調函數是否在動畫幀請求回調函數列表中,它的cancelled都會被設置爲true。

若是該handle沒有指向任何回調函數,則調用cancelAnimationFrame 不會發生任何事情。

處理模型

當頁面可見而且動畫幀請求回調函數列表不爲空時,瀏覽器會按期地加入一個「採樣全部動畫」的任務到UI線程的隊列中。

此處使用僞代碼來講明「採樣全部動畫」任務的執行步驟:

var list = {}; var browsingContexts = 瀏覽器頂級上下文及其下屬的瀏覽器上下文; for (var browsingContext in browsingContexts) {  var time = 從1970年1月1日到當前所通過的毫秒數;  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) {    }   }  } }

已解決的問題

  • 爲何在callback內部執行cancelAnimationFrame不能取消動畫?

問題描述

以下面的代碼會一直執行a:

 var id = null;  function a(time) {   console.log("animation");   window.cancelAnimationFrame(id); //不起做用   id = window.requestAnimationFrame(a);  }  a();

緣由分析

咱們來分析下這段代碼是如何執行的:

一、執行a

(1)執行「a();」,執行函數a;

(2)執行「console.log("animation");」,打印「animation」;

(3)執行「window.cancelAnimationFrame(id);」,由於id爲null,瀏覽器在動畫幀請求回調函數列表中找不到對應的callback,因此不發生任何事情;

(4)執行「id = window.requestAnimationFrame(a);」,瀏覽器會將一個元組< handle, a>插入到Document的動畫幀請求回調函數列表末尾,將id賦值爲該元組的handle值;

二、a執行完畢後,執行第一個「採樣全部動畫」的任務

假設當前頁面一直可見,由於動畫幀請求回調函數列表不爲空,因此瀏覽器會按期地加入一個「採樣全部動畫」的任務到線程隊列中。

a執行完畢後的第一個「採樣全部動畫」的任務執行時會進行如下步驟:

(1)拷貝Document的動畫幀請求回調函數列表到list變量中,清空Document的動畫幀請求回調函數列表;

(2)遍歷list的列表,列表有1個元組,該元組的callback爲a;

(3)判斷a的cancelled,爲默認值false,因此執行a;

(4)執行「console.log("animation");」,打印「animation」;

(5)執行「window.cancelAnimationFrame(id);」,此時id指向當前元組的a(即當前正在執行的a),瀏覽器將

當前元組

的a的cancelled設爲true。

 

(6)執行「id = window.requestAnimationFrame(a);」,瀏覽器會將

新的元組< handle, a>

插入到Document的動畫幀請求回調函數列表末尾(新元組的a的cancelled爲默認值false),將id賦值爲該元組的handle值。

三、執行下一個「採樣全部動畫」的任務

當下一個「採樣全部動畫」的任務執行時,會判斷動畫幀請求回調函數列表的元組的a的cancelled,由於該元組爲新插入的元組,因此值爲默認值false,所以會繼續執行a。

如此類推,瀏覽器會一直循環執行a。

解決方案

有下面兩個方案:

一、執行requestAnimationFrame以後再執行cancelAnimationFrame。

下面代碼只會執行一次a:

 var id = null;  function a(time) {   console.log("animation");   id = window.requestAnimationFrame(a);   window.cancelAnimationFrame(id);  }  a();

二、在callback外部執行cancelAnimationFrame。 下面代碼只會執行一次a:

function a(time) { console.log("animation"); id = window.requestAnimationFrame(a); } a(); window.cancelAnimationFrame(id);

由於執行「window.cancelAnimationFrame(id);」時,id指向了新插入到動畫幀請求回調函數列表中的元組的a,因此 「採樣全部動畫」任務判斷元組的a的cancelled時,該值爲true,從而再也不執行a。

注意事項

一、在處理模型 中咱們已經看到,在遍歷執行拷貝的動畫幀請求回調函數列表中的回調函數以前,Document的動畫幀請求回調函數列表已經被清空了。所以若是要屢次執行回調函數,須要在回調函數中再次調用requestAnimationFrame將包含回調函數的元組加入到Document的動畫幀請求回調函數列表中,從而瀏覽器纔會再次按期加入「採樣全部動畫」的任務(當頁面可見而且動畫幀請求回調函數列表不爲空時,瀏覽器纔會加入該任務),執行回調函數。

例以下面代碼只執行1次animate函數:

var id = null; function animate(time) { console.log("animation"); } window.requestAnimationFrame(animate);

下面代碼會一直執行animate函數:

var id = null; function animate(time) { console.log("animation"); window.requestAnimationFrame(animate); } animate();

二、若是在執行回調函數或者Document的動畫幀請求回調函數列表被清空以前屢次調用requestAnimationFrame插入同一個回調函數,那麼列表中會有多個元組指向該回調函數(它們的handle不一樣,但callback都爲該回調函數),「採集全部動畫」任務會執行屢次該回調函數。

例以下面的代碼在執行「id1 = window.requestAnimationFrame(animate);」和「id2 = window.requestAnimationFrame(animate);」時會將兩個元組(handle分別爲id一、id2,回調函數callback都爲animate)插入到Document的動畫幀請求回調函數列表末尾。 由於「採樣全部動畫」任務會遍歷執行動畫幀請求回調函數列表的每一個回調函數,因此在「採樣全部動畫」任務中會執行兩次animate。

 //下面代碼會打印兩次"animation"   var id1 = null,   id2 = null;  function animate(time) {   console.log("animation");  }   id1 = window.requestAnimationFrame(animate);  id2 = window.requestAnimationFrame(animate); //id1和id2值不一樣,指向列表中不一樣的元組,這兩個元組中的callback都爲同一個animate

兼容性方法

下面爲《HTML5 Canvas 核心技術》給出的兼容主流瀏覽器的requestNextAnimationFrame 和cancelNextRequestAnimationFrame方法,你們可直接拿去用:

window.requestNextAnimationFrame = (function () {  var originalWebkitRequestAnimationFrame = undefined,    wrapper = undefined,    callback = undefined,    geckoVersion = 0,    userAgent = navigator.userAgent,    index = 0,    self = this;  // Workaround for Chrome 10 bug where Chrome  // does not pass the time to the animation function  if (window.webkitRequestAnimationFrame) {   // Define the wrapper   wrapper = function (time) {    if (time === undefined) {     time = +new Date();    }    self.callback(time);   };   // Make the switch   originalWebkitRequestAnimationFrame = window.webkitRequestAnimationFrame;   window.webkitRequestAnimationFrame = function (callback, element) {    self.callback = callback;    // Browser calls the wrapper and wrapper calls the callback    originalWebkitRequestAnimationFrame(wrapper, element);   }  }  // Workaround for Gecko 2.0, which has a bug in  // mozRequestAnimationFrame() that restricts animations  // to 30-40 fps.  if (window.mozRequestAnimationFrame) {   // Check the Gecko version. Gecko is used by browsers   // other than Firefox. Gecko 2.0 corresponds to   // Firefox 4.0.   index = userAgent.indexOf('rv:');   if (userAgent.indexOf('Gecko') != -1) {    geckoVersion = userAgent.substr(index + 3, 3);    if (geckoVersion === '2.0') {     // Forces the return statement to fall through     // to the setTimeout() function.     window.mozRequestAnimationFrame = undefined;    }   }  }  return window.requestAnimationFrame ||    window.webkitRequestAnimationFrame ||    window.mozRequestAnimationFrame ||    window.oRequestAnimationFrame ||    window.msRequestAnimationFrame ||    function (callback, element) {     var start,       finish;     window.setTimeout(function () {      start = +new Date();      callback(start);      finish = +new Date();      self.timeout = 1000 / 60 - (finish - start);     }, self.timeout);    }; }());  window.cancelNextRequestAnimationFrame = window.cancelRequestAnimationFrame   || window.webkitCancelAnimationFrame   || window.webkitCancelRequestAnimationFrame   || window.mozCancelRequestAnimationFrame   || window.oCancelRequestAnimationFrame   || window.msCancelRequestAnimationFrame   || clearTimeout;
相關文章
相關標籤/搜索