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

爐火糖粥、 發表於 2020-06-30

一、前言

  前端實現動畫效果主要有以下幾種方法: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函式

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

 

 六、總結

  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);
};

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

     http://www.luyixian.cn/javascript_show_149688.aspx

     https://juejin.im/post/5c89fe42e51d455bb15c1ed1

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