JS 定時器的4種寫法及介紹

2021-04-07 15:00:53

JS提供了一些原生方法來實現延時去執行某一段程式碼,下面來簡單介紹一下setTiemout、setInterval、setImmediate、requestAnimationFrame。

一、什麼是定時器

JS提供了一些原生方法來實現延時去執行某一段程式碼,下面來簡單介紹一下 setTimeout: 設定一個定時器,在定時器到期後執行一次函數或程式碼段

var timeoutId = window.setTimeout(func[, delay, param1, param2, ...]);
var timeoutId = window.setTimeout(code[, delay]);
  • timeoutId: 定時器ID
  • func: 延遲後執行的函數
  • code: 延遲後執行的程式碼字串,不推薦使用原理類似eval()
  • delay: 延遲的時間(單位:毫秒),預設值為0
  • param1,param2: 向延遲函數傳遞而外的引數,IE9以上支援

setInterval: 以固定的時間間隔重複呼叫一個函數或者程式碼段

var intervalId = window.setInterval(func, delay[, param1, param2, ...]);
var intervalId = window.setInterval(code, delay);
  • intervalId: 重複操作的ID
  • func: 延遲呼叫的函數
  • code: 程式碼段
  • delay: 延遲時間,沒有預設值

setImmediate: 在瀏覽器完全結束當前執行的操作之後立即執行指定的函數(僅IE10和Node 0.10+中有實現),類似setTimeout(func, 0)

var immediateId = setImmediate(func[, param1, param2, ...]);
var immediateId = setImmediate(func);
  • immediateId: 定時器ID
  • func: 回撥

requestAnimationFrame: 專門為實現高效能的幀動畫而設計的API,但是不能指定延遲時間,而是根據瀏覽器的重新整理頻率而定(幀)

var requestId = window.requestAnimationFrame(func);

  • func: 回撥

上面簡單的介紹了四種JS的定時器,而本文將會主要介紹比較常用的兩種:setTimeout和setInterval。

二、舉個栗子

  • 基本用法

    // 下面程式碼執行之後會輸出什麼?
    var intervalId, timeoutId;

timeoutId = setTimeout(function () {

console.log(1);

}, 300);

setTimeout(function () {

clearTimeout(timeoutId);
console.log(2);

}, 100);

setTimeout('console.log("5")', 400);

intervalId = setInterval(function () {

console.log(4);
clearInterval(intervalId);

}, 200);

// 分別輸出: 2、4、5


*   setInterval 和 setTimeout的區別?

// 執行在面的程式碼塊會輸出什麼?
setTimeout(function () {

console.log('timeout');

}, 1000);

setInterval(function () {

console.log('interval')

}, 1000);

// 輸出一次 timeout,每隔1S輸出一次 interval

/--------------------------------/

// 通過setTimeout模擬setInterval 和 setInterval有啥區別麼?
var callback = function () {

if (times++ > max) {
    clearTimeout(timeoutId);
    clearInterval(intervalId);
}

console.log('start', Date.now() - start);
for (var i = 0; i < 990000000; i++) {}
console.log('end', Date.now() - start);

},
delay = 100,
times = 0,
max = 5,
start = Date.now(),
intervalId, timeoutId;

function imitateInterval(fn, delay) {

timeoutId = setTimeout(function () {
    fn();

    if (times <= max) {
        imitateInterval(fn ,delay);
    }
}, delay);

}

imitateInterval(callback, delay);
intervalId = setInterval(callback, delay);

如果是setTimeout和setInterval的話,它倆僅僅在執行次數上有區別,setTimeout一次、setIntervaln次。 而通過setTimeout模擬的setInterval與setInterval的區別則在於:setTimeout只有在回撥完成之後才會去呼叫下一次定時器,而setInterval則不管回撥函數的執行情況,當到達規定時間就會在事件佇列中插入一個執行回撥的事件,所以在選擇定時器的方式時需要考慮setInterval的這種特性是否會對你的業務程式碼有什麼影響?

*   setTimeout(func, 0) 和 setImmediate(func)誰更快?(僅僅是好奇,才寫的這段測試)

console.time('immediate');
console.time('timeout');

setImmediate(() => {

console.timeEnd('immediate');

});

setTimeout(() => {

console.timeEnd('timeout');

}, 0);

在Node.JS v6.7.0中測試發現setTimeout更早執行

*   面試題

下面程式碼執行後的結果是什麼?

// 題目一
var t = true;

setTimeout(function(){

t = false;

}, 1000);

while(t){}

alert('end');

/--------------------------------/

// 題目二
for (var i = 0; i < 5; i++) {

setTimeout(function () {
    console.log(i);
}, 0);

}

/--------------------------------/

// 題目三
var obj = {

msg: 'obj',
shout: function () {
    alert(this.msg);
},
waitAndShout: function() {
    setTimeout(function () {
        this.shout();
    }, 0);    
}

};
obj.waitAndShout();


問題答案會在後面解答

## 三、JS定時器的工作原理

在解釋上面問題的答案之前我們先來了解一下定時器的工作原理,這裡將用參照How JavaScript Timers Work中的例子來解釋定時器的工作原理,該圖為一個簡單版的原理圖。![Timers](https://upload-images.jianshu.io/upload_images/23129380-04ce157c5b931cdc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

上圖中,左側數位代表時間,單位毫秒;左側文字代表某一個操作完成後,瀏覽器去詢問當前佇列中存在哪些正在等待執行的操作;藍色方塊表示正在執行的程式碼塊;右側文字代表在程式碼執行過程中,出現哪些非同步事件。該圖大致流程如下:

*   程式開始時,有一個JS程式碼塊開始執行,執行時長約為18ms,在執行過程中有3個非同步事件觸發,其中包括一個setTimeout、滑鼠點選事件、setInterval
*   第一個setTimeout先執行,延遲時間為10ms,稍後滑鼠事件出現,瀏覽器在事件佇列中插入點選的回撥函數,稍後setInterval執行,10ms到達之後,setTimeout向事件佇列中插入setTimeout的回撥
*   當第一個程式碼塊執行完成後,瀏覽器檢視佇列中有哪些事件在等待,他取出排在佇列最前面的程式碼來執行
*   在瀏覽器處理滑鼠點選回撥時,setInterval再次檢查到到達延遲時間,他將再次向事件佇列中插入一個interval的回撥,以後每隔指定的延遲時間之後都會向佇列中插入一個回撥
*   後面瀏覽器將在執行完當前隊頭的程式碼之後,將再次取出目前隊頭的事件來執行

這裡只是對定時器的原理做一個簡單版的描述,實際的處理過程比這個複雜。

## 四、題目答案

好啦,我們現在再來看看上面的面試題的答案。 第一題

> alert永遠都不會執行,因為JS是單執行緒的,且定時器的回撥將在等待當前正在執行的任務完成後才執行,而while(t) {}直接就進入了死迴圈一直佔用執行緒,不給回撥函數執行機會

第二題

> 程式碼會輸出 5 5 5 5 5,理由同上,當i = 0時,生成一個定時器,將回撥插入到事件佇列中,等待當前佇列中無任務執行時立即執行,而此時for迴圈正在執行,所以回撥被擱置。當for迴圈執行完成後,佇列中存在著5個回撥函數,他們的都將執行console.log(i)的操作,因為當前JS程式碼上中並沒有使用塊級作用域,所以i的值在for迴圈結束後一直為5,所以程式碼將輸出5個5

第三題

> 這個問題涉及到this的指向問題,由setTimeout()呼叫的程式碼執行在與所在函數完全分離的執行環境上. 這會導致這些程式碼中包含的this關鍵字會指向window (或全域性)物件,window物件中並不存在shout方法,所以就會報錯,修改方案如下:

var obj = {

msg: 'obj',
shout: function () {
    alert(this.msg);
},
waitAndShout: function() {
    var self = this; // 這裡將this賦給一個變數
    setTimeout(function () {
        self.shout();
    }, 0);    
}

};
obj.waitAndShout();


## 五、需要注意的點

*   setTimeout有最小時間間隔限制,HTML5標準為4ms,小於4ms按照4ms處理,但是每個瀏覽器實現的最小間隔都不同
*   因為JS引擎只有一個執行緒,所以它將會強制非同步事件排隊執行
*   如果setInterval的回撥執行時間長於指定的延遲,setInterval將無間隔的一個接一個執行
*   this的指向問題可以通過bind函數、定義變數、箭頭函數的方式來解決