JavaScript 開發者應該知道的 setTimeout 祕密

TGCode發表於2017-01-12

計時器setTimeout是我們經常會用到的,它用於在指定的毫秒數後呼叫函式或計算表示式。

語法:setTimeout(code, millisec, args);

注意:如果code為字串,相當於執行eval()方法來執行code。

當然,這一篇文章並不僅僅告訴你怎麼用setTimeout,而且理解其是如何執行的。

1、setTimeout原理

先來看一段程式碼:

var start = new Date(); 
var end = 0; 
setTimeout(function() {  console.log(new Date() - start); }, 500); 
while (new Date() - start <= 1000) {}

在上面的程式碼中,定義了一個setTimeout定時器,延時時間是500毫秒。

你是不是覺得列印結果是: 500

可事實卻是出乎你的意料,列印結果是這樣的(也許你列印出來會不一樣,但肯定會大於1000毫秒):

這是為毛呢?究其原因,這是因為 JavaScript是單執行緒執行的。也就是說,在任何時間點,有且只有一個執行緒在執行JavaScript程式,無法同一時候執行多段程式碼。

再來看看瀏覽器下的JavaScript。

瀏覽器的核心是多執行緒的,它們在核心控制下相互配合以保持同步,一個瀏覽器至少實現三個常駐執行緒:JavaScript引擎執行緒,GUI渲染執行緒,瀏覽器事件觸發執行緒。

  • JavaScript引擎是基於事件驅動單執行緒執行的,JavaScript引擎一直等待著任務佇列中任務的到來,然後加以處理,瀏覽器無論什麼時候都只有一個JavaScript執行緒在執行JavaScript程式。
  • GUI渲染執行緒負責渲染瀏覽器介面,當介面需要重繪(Repaint)或由於某種操作引發迴流(Reflow)時,該執行緒就會執行。但需要注意,GUI渲染執行緒與JavaScript引擎是互斥的,當JavaScript引擎執行時GUI執行緒會被掛起,GUI更新會被儲存在一個佇列中等到JavaScript引擎空閒時立即被執行。
  • 事件觸發執行緒,當一個事件被觸發時,該執行緒會把事件新增到待處理佇列的隊尾,等待JavaScript引擎的處理。這些事件可來自JavaScript引擎當前執行的程式碼塊如setTimeout、也可來自瀏覽器核心的其他執行緒如滑鼠點選、Ajax非同步請求等,但由於JavaScript的單執行緒關係,所有這些事件都得排隊等待JavaScript引擎處理(當執行緒中沒有執行任何同步程式碼的前提下才會執行非同步程式碼)。

到這裡,我們再來回顧一下最初的例子:

var start = new Date();
 var end = 0;
 setTimeout(function() {  console.log(new Date() - start); }, 500);
 while (new Date() - start <= 1000) {}

雖然setTimeout的延時時間是500毫秒,可是由於while迴圈的存在,只有當間隔時間大於1000毫秒時,才會跳出while迴圈,也就是說,在1000毫秒之前,while迴圈都在佔據著JavaScript執行緒。也就是說,只有等待跳出while後,執行緒才會空閒下來,才會去執行之前定義的setTimeout。

最後 ,我們可以總結出,setTimeout只能保證在指定的時間後將任務(需要執行的函式)插入任務佇列中等候,但是不保證這個任務在什麼時候執行。一旦執行javascript的執行緒空閒出來,自行從佇列中取出任務然後執行它。

因為javascript執行緒並沒有因為什麼耗時操作而阻塞,所以可以很快地取出排隊佇列中的任務然後執行它,也是這種佇列機制,給我們製造一個非同步執行的假象。

2、setTimeout的好搭檔“0”

也許你見過下面這一段程式碼:

setTimeout(function(){ // statement}, 0);

上面的程式碼表示立即執行。本意是立刻執行呼叫函式,但事實上,上面的程式碼並不是立即執行的,這是因為setTimeout有一個最小執行時間,當指定的時間小於該時間時,瀏覽器會用最小允許的時間作為setTimeout的時間間隔,也就是說即使我們把setTimeout的延遲時間設定為0,被呼叫的程式也沒有馬上啟動。

不同的瀏覽器實際情況不同,IE8和更早的IE的時間精確度是15.6ms。不過,隨著HTML5的出現,在高階版本的瀏覽器(Chrome、ie9+等),定義的最小時間間隔是不得低於4毫秒,如果低於這個值,就會自動增加,並且在2010年及之後釋出的瀏覽器中採取一致。

所以說,當我們寫為 setTimeout(fn,0) 的時候,實際是實現插隊操作,要求瀏覽器“儘可能快”的進行回撥,但是實際能多快就完全取決於瀏覽器了。

setTimeout(fn, 0)有什麼用處呢?其實用處就在於我們可以改變任務的執行順序!因為瀏覽器會在執行完當前任務佇列中的任務,再執行setTimeout佇列中積累的的任務。

通過設定任務在延遲到0s後執行,就能改變任務執行的先後順序,延遲該任務發生,使之非同步執行。

來看一個網上很流行的例子:

document.querySelector('#one input').onkeydown = function() {  
     document.querySelector('#one span').innerHTML = this.value;
 }; 
document.querySelector('#second input').onkeydown = function() {    
     setTimeout(function() {  
         document.querySelector('#second span').innerHTML = document.querySelector('#second input').value; }, 0);
};

`例項:例項

當你往兩個表單輸入內容時,你會發現未使用setTimeout函式的只會獲取到輸入前的內容,而使用setTimeout函式的則會獲取到輸入的內容。

這是為什麼呢?

因為當按下按鍵的時候,JavaScript 引擎需要執行 keydown 的事件處理程式,然後更新文字框的 value 值,這兩個任務也需要按順序來,事件處理程式執行時,更新 value值(是在keypress後)的任務則進入佇列等待,所以我們在 keydown 的事件處理程式裡是無法得到更新後的value的,而利用 setTimeout(fn, 0),我們把取 value 的操作放入佇列,放在更新 value 值以後,這樣便可獲取出文字框的值。

未使用setTimeout函式,執行順序是:`onkeydown => onkeypress => onkeyup

使用setTimeout函式,執行順序是:onkeydown => onkeypress => function => onkeyup`

雖然我們可以使用keyup來替代keydown,不過有一些問題,那就是長按時,keyup並不會觸發。

長按時,keydown、keypress、keyup的呼叫順序:

keydown
keypress
keydown
keypress
...
keyup

也就是說keyup只會觸發一次,所以你無法用keyup來實時獲取值。

我們還可以用setImmediate()來替代setTimeout(fn,0)

if (!window.setImmediate) {  
    window.setImmediate = function(func, args){  
      return window.setTimeout(func, 0, args);  
   };  
  window.clearImmediate = window.clearTimeout;
 }

setImmediate()`方法用來把一些需要長時間執行的操作放在一個回撥函式裡,在瀏覽器完成後面的其他語句後,就立刻執行這個回撥函式,必選的第一個引數func,表示將要執行的回撥函式,它並不需要時間引數。

注意:目前只有IE10支援此方法,當然,在Nodejs中也可以呼叫此方法。

3、setTimeout的一些祕密

3.1 setTimeout中回撥函式的this

由於setTimeout() 方法是瀏覽器 window 物件提供的,因此第一個引數函式中的this其實是指向window物件,這跟變數的作用域有關。

看個例子:

var a = 1; 
var obj = {  
a: 2, 
 test: function() {  setTimeout(function(){  console.log(this.a);  }, 0);  
} 
}; 
obj.test(); // 1

不過我們可以通過使用bind()方法來改變setTimeout回撥函式裡的this

var a = 1; 
var obj = { 
 a: 2, 
 test: function() {  
setTimeout(function(){  
console.log(this.a);  
}.bind(this), 0);  
}
 }; 
obj.test(); // 2

3.2 setTimeout不止兩個引數

我們都知道,setTimeout的第一個引數是要執行的回撥函式,第二個引數是延遲時間(如果省略,會由瀏覽器自動設定。在IE,FireFox中,第一次配可能給個很大的數字,100ms上下,往後會縮小到最小時間間隔,Safari,chrome,opera則多為10ms上下。)

其實,setTimeout可以傳入第三個引數、第四個引數….,它們表示神馬呢?其實是用來表示第一個引數(回撥函式)傳入的引數。

setTimeout(function(a, b){  
console.log(a); // 3 
console.log(b); // 4},0, 3, 4);

相關文章