JS動畫三劍客——setTimeout、setInterval、requestAnimationFrame

一、前言

  前端實現動畫效果主要有以下幾種方法:CSS3中的transition 和 animation ,Javascript 中可以通過定時器 setTimeout、setinterval,HTML5 canvas,HTML5提供的requestAnimationFrame。本文主要分析setTimeout、setinterval、requestAnimationFrame三者的區別和他們各自的優缺點。在了解他們三個之前,我們先來看看一些相關概念。

二、相關概念介紹

  1.屏幕刷新頻率

    即圖像在屏幕上更新的速度,也即屏幕上的圖像每秒鐘出現的次數,它的單位是赫茲(Hz)。 對於一般筆記本電腦,這個頻率大概是60Hz。這個值的設定受屏幕分辨率、屏幕尺寸和顯卡的影響。

  2.動畫原理

    動畫本質就是要讓人眼看到圖像被刷新而引起變化的視覺效果,這個變化要以連貫的、平滑的方式進行過渡。在屏幕每次刷新前,將圖像的位置向左移動一個像素,即1px。屏幕每次刷出來的圖像位置都比前一個要差1px,你就會看到圖像在移動;由於我們人眼的視覺停留效應,當前位置的圖像停留在大腦的印象還沒消失,緊接着圖像又被移到了下一個位置,因此你才會看到圖像在流暢的移動,這就是視覺效果上形成的動畫。

三、setInterval

  1.運行機制

    按照指定的周期(以毫秒計)來調用函數或計算表達式。方法會不停地調用函數(當頁面被隱藏或者最小化時,setInterval()仍在後台繼續執行,這種動畫刷新是完全沒有意義的,對cpu也是極大的浪費),直到 clearInterval() 被調用或窗口被關閉。

    setinterval的執行時間不確定,參數中的時間間隔是將代碼添加到異步隊列中等待的時間。只有當主線程中的任務以及隊列前面的任務是執行完畢,才真正開始執行動畫代碼。

    註:HTML5標準規定,setInterval的最短間隔時間是10毫秒,也就是說,小於10毫秒的時間間隔會被調整到10毫秒。

  2.語法

    setinterval(code, milliseconds);

    setinterval(function, milliseconds, param1, param2, …)

參數 描述
code/function 必需。要調用一個代碼串,也可以是一個函數。
milliseconds 必須。周期性執行或調用 code/function 之間的時間間隔,以毫秒計。
param1, param2, … 可選。 傳給執行函數的其他參數(IE9 及其更早版本不支持該參數)。

  3.實例

//每三秒(3000 毫秒)彈出 "Hello":
var myVar;
 
function myFunction() {
    myVar = setInterval(alertFunc, 3000);
}
 
function alertFunc() {
    alert("Hello!");
}

  4.清除setInterval

    clearinterval() 方法可取消由 setinterval() 函數設定的定時執行操作。參數必須是由 setinterval() 返回的 id 值。 注意: 要使用 clearinterval() 方法, 在創建執行定時操作時要使用全局變量.清除示例如下:

var myVar = setInterval(function(){ setColor() }, 300);
 
function setColor() {
    var x = document.body;
    x.style.backgroundColor = x.style.backgroundColor == "yellow" ? "pink" : "yellow";
}
 
function stopColor() {
    clearInterval(myVar);
}

  5.缺點

   (1)setinterval()無視代碼錯誤,如果setinterval執行的代碼由於某種原因出了錯,它還會持續不斷地調用該代碼。

   (2)setinterval無視網絡延遲,由於某些原因(服務器過載、臨時斷網、流量劇增、用戶帶寬受限,等等),你的請求要花的時間遠比你想像的要長。但setinterval不在乎。它仍然會按定時持續不斷地觸發請求,最終你的客戶端網絡隊列會塞滿調用函數。

   (3) setinterval不保證執行,與settimeout不同,並不能保證到了時間間隔,代碼就准能執行。如果你調用的函數需要花很長時間才能完成,那某些調用會被直接忽略 

四、setTimeout

  1.運行機制

    在指定的毫秒數後調用函數或計算表達式。每次函數執行的時候都會創建換一個新的定時器。在前一個定時器代碼執行完之前,不會向隊列插入新的定時器代碼,確保不會有任何確實的間隔。並且確保在下一次定時器代碼執行之前,至少要等待指定的間隔,避免了連續的運行。當方法執行完成定時器就立即停止(但是定時器還在,只不過沒用了);

  2.語法(同setInterval)

  3.實例

//3 秒(3000 毫秒)後彈出 "Hello" :
var myVar;
 
function myFunction() {
    myVar = setTimeout(alertFunc, 3000);
}
 
function alertFunc() {
    alert("Hello!");
}

  4.清除setTimeout

    使用cleartimeout函數,用法同clearinterval

  5.缺點

    (1)利用seTimeout實現的動畫在某些低端機上會出現卡頓、抖動的現象。

    (2)settimeout的執行時間並不是確定的。在javascript中, settimeout 任務被放進了異步隊列中,只有當主線程上的任務執行完以後,才會去檢查該隊列里的任務是否需要開始執行,因此 settimeout 的實際執行時間一般要比其設定的時間晚一些。

     (3)刷新頻率受屏幕分辨率和屏幕尺寸的影響,因此不同設備的屏幕刷新頻率可能會不同,而 settimeout只能設置一個固定的時間間隔,這個時間不一定和屏幕的刷新時間相同。

     (4)settimeout的執行只是在內存中對圖像屬性進行改變,這個變化必須要等到屏幕下次刷新時才會被更新到屏幕上。如果兩者的步調不一致,就可能會導致中間某一幀的操作被跨越過去,而直接更新下一幀的圖像。

五、requestAnimationFrame(推薦使用)

  1.運行機制

    告訴瀏覽器——你希望執行一個動畫,並且要求瀏覽器在下次重繪之前調用指定的回調函數更新動畫。不需要設置時間間隔,是由系統的時間間隔定義的。大多數瀏覽器的刷新頻率是60Hz(每秒鐘反覆繪製60次),循環間隔是1000/60,約等於16.7ms。不需要調用者指定幀速率,瀏覽器會自行決定最佳的幀效率。只被執行一次,這樣就不會引起丟幀現象,也不會導致動畫出現卡頓的問題。

  2.語法

    window.requestanimationframe(callback);

    參數callback:下一次重繪之前更新動畫幀所調用的函數(即上面所說的回調函數)。

  3.實例

var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';

function step(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

  4.缺點

   requestanimationframe 不管理回調函數,即在回調被執行前,多次調用帶有同一回調函數的 requestanimationframe,會導致回調在同一幀中執行多次。我們可以通過一個簡單的例子模擬在同一幀內多次調用 requestanimationframe 的場景:(mousemove, scroll 這類事件常見)

const animation = timestamp => console.log('animation called at', timestamp)
 
window.requestAnimationFrame(animation)
window.requestAnimationFrame(animation)
// animation called at 320.7559999991645
// animation called at 320.7559999991645 

   我們用連續調用兩次 requestanimationframe 模擬在同一幀中調用兩次 requestanimationframe。 例子中的 timestamp 是由 requestanimationframe 傳給回調函數的,表示回調隊列被觸發的時間。由輸出可知,animation 函數在同一幀內被執行了兩次,即繪製了兩次動畫。

  ps:解決辦法

    對於這種高頻發事件,一般的解決方法是使用節流函數。但是在這裡使用節流函數並不能完美解決問題。因為節流函數是通過時間管理隊列的,而 requestanimationframe 的觸發時間是不固定的,在高刷新頻率的顯示屏上時間會小於 16.67ms,頁面如果被推入後台,時間可能大於 16.67ms。

    完美的解決方案是通過 requestanimationframe 來管理隊列,其思路就是保證 requestanimationframe 的隊列里,同樣的回調函數只有一個。示例代碼如下:

const onScroll = e => {
    if (scheduledAnimationFrame) { return }
 
    scheduledAnimationFrame = true
    window.requestAnimationFrame(timestamp => {
        scheduledAnimationFrame = false
        animation(timestamp)
    })
}
window.addEventListener('scroll', onScroll)

  5.與setTimeout和setInterval的區別

    (1)requestanimationframe會把每一幀中的所有dom操作集中起來,在一次重繪或迴流中就完成,並且重繪或迴流的時間間隔緊緊跟隨瀏覽器的刷新頻率

    (2)在隱藏或不可見的元素中,requestanimationframe將不會進行重繪或迴流,這當然就意味着更少的cpu、gpu和內存使用量

    (3)requestanimationframe是由瀏覽器專門為動畫提供的api,在運行時瀏覽器會自動優化方法的調用,並且如果頁面不是激活狀態下的話,動畫會自動暫停,有效節省了cpu開銷

  6.兼容性封裝

if(!window.requestAnimationFrame) {
 window.requestAnimationFrame = (window.webkitRequestAnimationFrame ||
 window.mozRequestAnimationFrame ||
 window.oRequestAnimationFrame ||
 window.msRequestAnimationFrame ||
 function(callback) {
  var self = this, start, finish;
  return window.setTimeout(function() {
   start = +new Date();
   callback(start);
   finish = +new Date();
   self.timeout = 1000/60 - (finish - start);
  }, self.timeout);
 });
}

  代碼解析:

    這段代碼先檢查了 window.requestanimationframe 函數的定義是否存在。如果不存在,就遍歷已知的各種瀏覽器實現並替代該函數。如果還是找不到一個與瀏覽器相關的實現,它最終會採用基於javascript定時器的動畫以每秒60幀的間隔調用settimeout函數。

    mozrequestanimationframe() 會接收一個時間碼(從1970年1月1日起至今的毫秒數),表示下一次重繪的實際發生時間。這樣, mozrequestanimationframe() 就會根據這個時間碼設定將來的某個時刻進行重繪。

     但是 webkitrequestanimationframe() 和 msrequestanimationframe() 不會給回調函數傳遞時間碼,因此無法知道下一次重繪將發生在什麼時間。 如果要計算兩次重繪的時間間隔,firefox中可以使用既有的時間碼,而在chrome和ie則可以使用不太精確地date()對象。

  7.清除動畫

    cancelAnimationFrame(動畫名) ,類似clearTimeout函數

    

 

 六、總結

  1.執行次數:setInterval執行多次,setTimeout、requestAnimationframe執行一次

  2.性能:setTimeout會出現丟幀、卡頓現象,setInterval會出現調用丟失情況,requestAnimationframe不會出現這些問題,頁面未激活時不會執行動畫,減少了大量cpu消耗

  3.兼容性問題:setInterval,setTimeout在IE瀏覽器中不支持參數傳遞,能夠在大多數瀏覽器中正常使用。而requestAnimationframe不兼容IE10以下

七、面試題

  1.setTimeout中的this指向問題

var i = 0;
const o = {
    i: 1;
    fn: function(){
        console.log(this.i);
    }
}
setTimeout(o.fn, 1000); //執行後會打印出什麼

    錯誤思路:setTimeout執行,調用對象O的fn函數,由於調用者是對象O,那麼this也指向了對象O,又對象O中有屬性i,則會打印出1。

    正解:因為setTimeout是window對象的方法,傳入o.fn只是將o.fn這個函數傳給了setTimeout,仍然是window對象在調用。上面代碼執行的正確結果是0,是因為定義了全局變量i為0。如果沒有定義,則會輸出undefined。

    ps:如果這裡不是setTimeout執行這個函數,而是o.fn(),那麼會輸出1。

  2.執行下面的代碼,控制台如何輸出

(function () {
    setTimeout(function () {
        alert(2);
    }, 0);

    alert(1);
})()  

    先彈出的應該是1,而不是你以為「立即執行」的2。 settimeout,setinterval都存在一個最小延遲的問題,雖然你給的delay值為0,但是瀏覽器執行的是自己的最小值。html5標準是4ms,但並不意味着所有瀏覽器都會遵循這個標準,包括手機瀏覽器在內,這個最小值既有可能小於4ms也有可能大於4ms。在標準中,如果在settimeout中嵌套一個settimeout, 那麼嵌套的settimeout的最小延遲為10ms。

  3.執行下面的代碼,控制台輸出什麼

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

    輸出結果大家都只是會是5個6,由於JavaScript是單線程的,按順序執行,setTimeout是異步函數,它會將 timer 函數放到任務隊列中,而此時會先將循環執行完畢再執行 timer 函數,因此當執行 timer 函數時 i 已經等於6了,所以最終會輸出5個6

    ps:解決辦法有三種,我只貼代碼了

//閉包
for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

//給setTimeout傳參
//方式一 IE不支持
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}
//方式二

for (var i = 1; i <= 5; i++) {
  (function(i){
    setTimeout(function(){
        console.log(i)
    },i * 1000)
  })(i) }
//ES6 let

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

  4.使用settimeout代替setinterval進行間歇調用

var executeTimes = 0;
var intervalTime = 500;
var intervalId = null;

// 放開下面的注釋運行setInterval的Demo
intervalId = setInterval(intervalFun,intervalTime);
// 放開下面的注釋運行setTimeout的Demo
// setTimeout(timeOutFun,intervalTime);

function intervalFun(){
    executeTimes++;
    console.log("doIntervalFun——"+executeTimes);
    if(executeTimes==5){
        clearInterval(intervalId);
    }
}

function timeOutFun(){
    executeTimes++;
    console.log("doTimeOutFun——"+executeTimes);
    if(executeTimes<5){
        setTimeout(arguments.callee,intervalTime);
    }
}

  代碼比較簡單,我們只是在settimeout的方法裏面又調用了一次settimeout,就可以達到間歇調用的目的。 setinterval間歇調用,是在前一個方法執行前,就開始計時,比如間歇時間是500ms,那麼不管那時候前一個方法是否已經執行完畢,都會把後一個方法放入執行的序列中。這時候就會發生一個問題,假如前一個方法的執行時間超過500ms,加入是1000ms,那麼就意味着,前一個方法執行結束後,後一個方法馬上就會執行,因為此時間歇時間已經超過500ms了。

  5.利用settimeout來實現setinterval

function interval(func, w, t){
    var interv = function(){
        if(typeof t === "undefined" || t-- > 0){
            setTimeout(interv, w);
            try{
                func.call(null);
            }
            catch(e){
                t = 0;
                throw e.toString();
            }
        }
    };

    setTimeout(interv, w);
};

參考文檔://blog.csdn.net/weixin_34204057/article/details/89009605

     //www.luyixian.cn/javascript_show_149688.aspx

     //juejin.im/post/5c89fe42e51d455bb15c1ed1

     //www.cnblogs.com/icctuan/p/12103697.html  

     

Tags: