前端計劃——JavaScript中關於setTimeout的那些事

前言:setTimeout是JavaScript中常見的一個window對象方法,本文將介紹關於它的一些基礎知識和易出錯的地方。瀏覽器

一、基礎知識

做用:setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式。 安全

基本語法:多線程

let timeoutId = window.setTimeout(func[, delay, param1, params2, ...]);
let timeoutId = scope.setTimeout(code[, delay]);
let timeoutId = window.setTimeout(function, milliseconds);
  • timeoutID 是該延時操做的數字ID, 此ID隨後能夠用來做爲window.clearTimeout方法的參數。異步

  • func是你想要在delay毫秒以後執行的函數。函數

  • code 在第二種語法,是指你想要在delay毫秒以後執行的代碼字符串,(使用該語法是不推薦的,
    不推薦的緣由和eval()同樣,即:一、安全性差(可被植入惡意代碼)二、執行效率低(須要將字符串解析爲代碼再執行))oop

  • delay 是延遲的毫秒數(1秒=1000毫秒),函數的調用會在該延遲以後發生。若是省略該參數,delay取默認值0。this

  • 須要注意的是,IE9 及更早的 IE 瀏覽器不支持第一種語法中向延遲函數傳遞額外參數的功能。插件

//如下是一個簡單實例,3秒後彈窗提示
function myFunction(){
    setTimeout(function(){alert("Hello")},3000);
}

二、單線程與事件隊列機制

先來看一些程序線程

//請判斷如下代碼輸出結果
setTimeout(function(){
    alert("Hello World");
   },1000);
   while(true){};
//該函數會陷入死循環,1秒後並不會彈出提醒
//請寫出如下代碼輸出結果
setTimeout(function (){
    console.log('a')
},0)
console.log('b')
//輸出結果爲b,a

有同窗可能會認爲:第一段代碼在1秒後會彈窗提示Hello World。而第二段代碼,把延遲毫秒數設爲0,就會當即執行,先輸出a,再輸出b。顯然,實際的結果不是這樣。 code

爲何呢?由於:

JavaScript引擎是單線程運行的,瀏覽器不管在何時都只且只有一個線程在運行JavaScript程序。

咱們先來介紹下瀏覽器渲染時的線程機制:
瀏覽器的內核是多線程的,它們在內核控制下相互配合以保持同步,一個瀏覽器至少實現三個常駐線程:JavaScript引擎線程,GUI渲染線程,瀏覽器事件觸發線程。

  • JavaScript引擎是基於事件驅動單線程執行的,JavaScript引擎一直等待着任務隊列中任務的到來,而後加以處理,瀏覽器不管何時都只有一個JavaScript線程在運行JavaScript程序。

  • GUI渲染線程負責渲染瀏覽器界面,當界面須要重繪(Repaint)或因爲某種操做引起迴流(Reflow)時,該線程就會執行。但須要注意,GUI渲染線程與JavaScript引擎是互斥的,當JavaScript引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JavaScript引擎空閒時當即被執行。

  • 事件觸發線程,當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JavaScript引擎的處理。這些事件可來自JavaScript引擎當前執行的代碼塊如setTimeout、也可來自瀏覽器內核的其餘線程如鼠標點擊、Ajax異步請求等,但因爲JavaScript的單線程關係全部這些事件都得排隊等待JavaScript引擎處理(當線程中沒有執行任何同步代碼的前提下才會執行異步代碼)。
    JavaScript引擎是基於事件驅動單線程執行的,JavaScript引擎一直等待着任務隊列中任務的到來,而後加以處理,瀏覽器不管何時都只有一個JavaScript線程在運行JavaScript程序。

迴歸開始的問題:

  • 第一段代碼始終會執行同步代碼,陷入死循環,根本不會執行setTimeout內的函數,也就不會彈窗。

  • 第二段代碼,通過查找資料,能夠得知,setTimeout有一個最小執行時間,當指定的時間小於該時間時,瀏覽器會用最小容許的時間做爲setTimeout的時間間隔,也就是說——即便咱們把setTimeout的毫秒數設置爲0,被調用的程序也沒有立刻啓動,它仍然會放在事件隊列的最後。當同步代碼執行完,輸出b,而後再執行延遲函數,輸出a。

三、this

this是JavaScript中一個重要的考察點,能夠簡單記爲:this是指向函數執行時的當前對象,假若沒有明確的當前對象,它就是指向window的。

請看以下代碼

//求函數執行結果
var a=1;
var obj={
    a:2,
    b:function(){
        setTimeout(function(){
            console.log(this.a);
        },2000);
        
    }
};
obj.b();
//函數輸出爲1

由setTimeout()調用的代碼運行在與所在函數徹底分離的執行環境上。這會致使,這些代碼中包含的 this 關鍵字會指向 window (或全局)對象,這和所指望的this的值是不同的。也就是當執行時,this.a並不能讀取obj對象中的a,而是會找到全局對象的a,故輸出結果爲1。
爲了改變這種狀況,有如下兩種方法

//方法一
var a=1;
var obj={
    var me = this;
    a:2,
    b:function(){
        setTimeout(function(){
            console.log(me.a);
        },2000);
        
    }
};
obj.b();
//方法二
var a=1;
var obj={
    a:2,
    b:function(){
        setTimeout(function(){
            console.log(this.a);
        }.bind(this),2000);
 
    }
};
obj.b();

四、混合知識點考察

題目一:求輸出結果

(function() {
    console.log(1); 
    setTimeout(function(){console.log(2)}, 1000); 
    setTimeout(function(){console.log(3)}, 0); 
    console.log(4);
})();
//輸出結果是1,4,3,2

解釋:參照第二部分的事件機制

題目二:對setTimeout和IIFE的考察

將 JavaScript 代碼包含在一個函數塊中有什麼用途?爲何要這麼作?
換句話說,爲何要用當即執行函數表達式(Immediately-Invoked Function Expression)。
IIFE 有兩個比較經典的使用場景,一是相似於在循環中定時輸出數據項,二是相似於 JQuery/Node 的插件和模塊開發。

for(var i = 0; i < 5; i++) {
 setTimeout(function() {
 console.log(i); 
 }, 1000);
}
//輸出爲5,5,5,5,5

使用IIFE

for(var i = 0; i < 5; i++) {
 (function(i) {
 setTimeout(function() {
 console.log(i); 
 }, 1000);
 })(i)
}
//輸出爲0,1,2,3,4

題目三:setTimeout的分片應用

若是 list 很大,下面的這段遞歸代碼會形成堆棧溢出。若是在不改變遞歸模式的前提下修善這段代碼?

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();
    if (item) {
    // process the list item...
    nextListItem();
    }
};

解決方案:加入定時器

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();
    if (item) {
    // process the list item...
    setTimeout(nextListItem(), 0);
    }
};

題目四:考察setTimeout和Promise系列

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

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three

解釋:當即resolve的Promise對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時。上面代碼中,setTimeout(fn, 0)在下一輪「事件循環」開始時執行,Promise.resolve()在本輪「事件循環」結束時執行,console.log('one')則是當即執行,所以最早輸出。

題目五:setTimeout與箭頭函數的this指向

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭頭函數
  setInterval(() => this.s1++, 1000);
  // 普通函數
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);

上面代碼中,Timer函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。前者的this綁定定義時所在的做用域(即Timer函數),後者的this指向運行時所在的做用域(即全局對象)。因此,3100毫秒以後,timer.s1被更新了3次,而timer.s2一次都沒更新。

相關文章
相關標籤/搜索