第三十九課:requestAnimationFrame詳解


大家應該都知道,如果一個頁面運行的定時器很多,無論你怎么優化,最后肯定會超過指定時間才能完成動畫。定時器越多,延時越嚴重。

為此,YUI,kissy等采用中央隊列的方式,將定時器減少至一個。瀏覽器廠商也因此原生支持了requestAnimationFrame方法,此方法基本上能保證每秒刷新60次。但是此方法在還沒形成標准之前,很多低版本瀏覽器是不支持的,比如:IE9以及以下版本,不過谷歌和火狐都用私有的方法名實現了requestAnimationFrame方法。比如:谷歌:webkitRequestAnimationFrame,,火狐:mozRequestAnimationFrame。形成標准后,IE10才開始支持,由於IE10支持的是標准的requestAnimationFrame方法,因此它沒有私有前綴,所以並不存在msRequestAnimationFrame。

我們先來看一下requestAnimationFrame方法是如何使用的?

var startTime,duration = 3000,requestID;

function animate(now){   //webkitRequestAnimationFrame方法會給回調函數中傳入一個當前時間的參數。

  var per = (now - startTime) / duration;

  if(per >=1){

    //動畫結束

  }else{

    div.style.left = Math.round(600*per) + "px";

    window.webkitRequestAnimationFrame(animate);   //此方法調用一次只會重繪一次動畫,如果需要連續的動畫,則需要重復調用

  }

}

function start(){

  startTime = Date.now();

  requestID = window.webkitRequestAnimationFrame(animate);  //此方法可以傳入兩個參數,第一個是回調,第二個是執行動畫的元素節點(可選),返回一個ID。

}

div.onclick = start;

上面的這個例子,是針對chrome瀏覽器實現的。

那么,我們如何來寫出兼容性的寫法呢?

第一個版本:

window.requestAnimationFrame = (function(){

  return window.requestAnimationFrame ||    //IE10以及以上版本,以及最新谷歌,火狐版本

      window.webkitRequestAnimationFrame ||   //谷歌老版本

       window.mozRequestAnimationFrame ||   //火狐老版本

        function(callback){    //IE9以及以下版本

          window.setTimeout(callback , 1000/60);  //這里強制讓動畫一秒刷新60次,這里之所以設置為16.7毫秒刷新一次,是因為requestAnimationFrame默認也是16.7毫秒刷新一次。

        }

})();

上面這個兼容性寫法,有幾個問題,第一個:沒有解決cancelAnimationFrame方法的兼容性寫法。第二個:強制讓IE9-瀏覽器,動畫繪制間隔為16.7ms,但是這些瀏覽器的繪制間隔並不都是這個值。第三個:火狐老版本的mozRequestAnimationFrame方法跟標准的requestAnimationFrame方法實現有些出入,比如:早期火狐的此方法,不支持傳參。第四個:老版本的webkit,在有些版本下,此方法不會返回id,還有一些版本沒有給回調函數傳當前時間的參數。

我的理解:至於老版本的火狐和老版本的webkit,我個人覺得沒有必要去兼容,只要兼容IE9-瀏覽器就OK了。因此以上的4個問題,只存在前面兩個。那如果你想兼容第三個和第四個問題的話,請去看司徒正美基於網友屈屈與月影的版本改進而來的版本:https://github.com/wedteam/qwrap-components/blob/master/animation/anim.frame.js。

第二個版本,解決上面的第一個問題和第二個問題:

(function() {
  var lastTime = 0;
  var version = ['webkit', 'moz'];
  for(var i = 0; i < version.length && !window.requestAnimationFrame; i++) {   //如果此瀏覽器不支持requestAnimationFrame方法,就循環遍歷version數組
    window.requestAnimationFrame = window[version[i] + 'RequestAnimationFrame'];
    window.cancelAnimationFrame = window[version [i] + 'CancelAnimationFrame'] ||         // 有一些Webkit版本中,此方法的名字改變了
      window[version [i] + 'CancelRequestAnimationFrame'];
  }

  if (!window.requestAnimationFrame) {   //如果是IE9-瀏覽器
    window.requestAnimationFrame = function(callback, element) {      //我們使用上一個例子來講解這段代碼。當我們點擊div時,就會觸發start方法,我們假設當前時間為11111,設置startTime=11111, 調用requestAnimationFrame(animate)方法,這時,當前時間,我們假設是currTime = 11112,lastTime = 0,這時timeToCall = 0,因此調用setTimeout(function(){},0),把lastTime = 11111,返回id。過了瀏覽器的最小時間后,我們假設是4ms,就會立即執行animate(11112)。這時就會繼續執行requestAnimationFrame。假設當前時間是11118,timeToCall = 9.7,這時lastTime = 11127.7,當前時間為11127.7時,就執行animate(11127.7),per = 16.7 / 3000,繼續執行requestAnimationFrame....
      var currTime = new Date().getTime();
      var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));   //timeToCall的值為0-16.7之間。

      var id = window.setTimeout(function() {
        callback(currTime + timeToCall);
      }, timeToCall);
      lastTime = currTime + timeToCall;
      return id;
    };
  }
  if (!window.cancelAnimationFrame) {   
    window.cancelAnimationFrame = function(id) {
      clearTimeout(id);
    };
  }
})();

當然,requestAnimationFrame不是沒有缺點,它不能控制fps(60),我們在以下場景下就不能使用。比如:做一些慢放動作,fps<60的情況下;還有在動作,槍戰,飛車等場景下,fps需要>60的情況下,像這種場景下,如果幀數不高,畫面會模糊。利用setTimeout(IE9,10,Firefox,chrome等,它的最短時間間隔已經壓縮至4ms了)我們可以輕松跑到100幀以上的動畫,能讓畫面更清楚,細節更逼真。

另外,postMessage這個異步方法,能實現超高度的動畫,有人做過實驗:

setTimeout                      平均幀數200                               

requestAnimationFrame    平均幀數60           

loop(循環)                     平均幀數200-300                         

postMessage                    平均幀數900-1000

var testing = true;   //用來停止動畫的,也就是停止代碼執行的

function main(){

  //記錄兩次執行時間的間隔

}

function run1(){       //點擊按鈕1,執行run1方法,然后使用setTimeout方法不斷的執行main方法,main方法會記錄每次執行的時間,求出兩次執行時間的間隔。

  main();

  if(testing){

    setTimeout(run1, 1);

  }

}

function run2(){    //點擊按鈕2,執行run2方法

  main();

  if(testing){

    window.requestAnimationFrame(run2);

  }

}

function run3(){   //點擊按鈕3,執行run3方法

  var count = 15;

  while(count--){         //利用while循環執行main方法,記錄兩個循環操作之間的時間間隔。

    main();

  }

  if(testing){

    setTimeout(run3,1);    //當然這里會有一點點的誤差,因為用到了setTimeout方法,這樣我們可以設置testing=false,停止循環調用main,如果直接用while(true),那么無法停止此循環。

  }

}

window.addEventListener("message",run4,false);   //綁定message事件,只要調用postMessage方法,就會觸發message事件。

function run4(){

  main();

  if(testing){

    window.postMessage("","*");

  }

}

IE10也有一個高效的異步方法setImmediate。

在現實中,尤其是游戲開發,我們要結合多種異步API。比如:作為背景的樹木,流水等用requestAnimationFrame方法,玩家角色,由於需要速度的變化,那么用setTimeout比較合適,一些非常炫的動畫,可能就需要postMessage,setImmediate,Image.onerror等API了。

 

 

 

加油!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM