最近都在看一些JavaScript原理層面的文章,恰巧看到了jQuery的作者的一篇關於JavaScript計時器原理的解析,於是誠惶誠恐地決定把原文翻譯成中文,一來是為了和大家分享,二來是為了加深自己對於JavaScript的理解。原文鏈接:http://ejohn.org/blog/how-javascript-timers-work/
原文翻譯:
從基礎層面來講,理解JavaScript計時器的工作原理是很重要的。由於JavaScript是單線程的,所以很多時候計時器並不是表現得和我們的直觀想象一樣。讓我們從下面的三個函數開始,它們能夠讓我們有機會去構造和操作計時器。
- var id =setTimeout(fn, delay); -創建了一個簡單的計時器,在經過給定的時間后,回調函數將會被執行。這個函數會返回一個唯一的ID,便於在之后某個時間可以注銷這個計時器。
- var id = setInterval(fn, delay); -和setTimeout類似,但是每經過一段時間(給定的延時),所傳遞的函數就會被執行一次,直到這個定時器被注銷。
clearInterval(id);
,clearTimeout(id); -接受一個計時器ID(由之前兩種計時器返回)並且停止計時器回調函數的執行。
為了理解計時器的內部工作原理,我們首先需要了解一個非常重要的概念:計時器設定的延時是沒有保證的。因為所有在瀏覽器中執行的JavaScript單線程異步事件(比如鼠標點擊事件和計時器)都只有在它有空的時候才執行。這最好通過圖片來說明,就如下面這張圖所示:
這一張圖片里面有很多信息需要慢慢消化,但是徹底地理解這張圖片將會讓你對JavaScript異步執行是如何工作的有一個更好的認識。這張圖片是從一維的角度來闡述的:在垂直方向是以毫秒計的時間,藍色的塊代表了
當前正在執行的JavaScript代碼段。比如第一段JavaScript執行了大概18毫秒,鼠標點擊事件大概執行了11毫秒。
由於JavaScript每次只能執行一段代碼(基於它單線程的特性),所以所有這些代碼段都阻塞了其他異步事件的執行。這就意味着,當一件異步事件(比如鼠標點擊,計時器觸發和一個XMLHttpRequest 請求完成)觸發的時候,這些事件的回調函數將排在執行隊列的最后去等待執行(排隊的方式因瀏覽器不同而不同,這里只是一個簡化)。
一開始,在第一段代碼段內,兩個計時器被初始化:一個10ms的setTimeout
和一個10ms的setInterval。由於計時器在哪兒初始化就在那兒開始計時,所以實際上計時器在第一段代碼執行完成之前就觸發了。然而,計時器的回調函數並不是立即執行了(單線程限制了不能這樣做),相反的是,回調函數排在了執行隊列的最后,等到下一個有空的時間去執行。
此外,在第一個代碼塊內我們看到了一個鼠標點擊事件發生了。與之相關的javascript異步事件(我們不可能預測用戶會在什么時候去采取這樣的動作,因此這個事件被視為異步的)並不會立即執行。和計時器一樣的是,它被放到了隊列的最后去等待執行。
在第一個代碼快執行完成的時候,瀏覽器會立即發出這樣的詢問:誰正在等待執行?這個時候,鼠標點擊處理程序和計時器回調函數都在等待執行。瀏覽器選擇了其中一個(鼠標點擊回調函數)並且立即執行它。為了執行,計時器會等到下一個可能執行的時間。
我們注意到,當鼠標點擊事件對應的處理程序正在執行的時候,第一個定時回調函數也要執行了。同定時計時器一樣,它也在隊列的后面等待執行。然而,我們可以注意到,當定時器再一次觸發(在計時器回調函數正在執行的時候),這一次定時器回調函數被丟棄了。如果在執行一大塊代碼塊的時候,你把所有的定時回調函數都放在隊列的最后,結果就是一大串定時回調函數將會沒有間隔的一起執行,直到完成。相反,在把更多定時回調函數放到隊列之前,瀏覽器會靜靜的等待,知道隊列中的所有定時回調函數都執行完成。
事實上,我們可以看到,當interval回調函數正在執行的時候,interval第三次被觸發。這給我們一個很重要的信息:interval並不關心當前誰在執行,它的回調函數會不加區分地進入隊列,即使存在這個回調函數會被丟棄的可能。
最后,當第二個定時回調函數完成執行的時候,我們可以看到javascript引擎已經沒有什么需要執行了。這意味着,瀏覽器現在正在等待一個新的異步事件的發生。我們可以看到在50ms的時候,定時回調函數再一次被觸發。然而,這一次,沒有其他代碼阻塞他的執行了,所以他立即執行了定時回調函數。
讓我們看一個例子來更好地闡述setTimeout
和setInterval的區別。
1 setTimeout(function(){ 2 /* Some long block of code... */ 3 setTimeout(arguments.callee, 10); 4 }, 10); 5 6 setInterval(function(){ 7 /* Some long block of code... */ 8 }, 10);
第一眼看上去這兩段代碼在功能上是等價的,但事實上卻不是。值得注意的是,setTimeout
這段代碼會在每次回調函數執行之后至少需要延時10ms再去執行一次(可能是更多,但是不會少)。但是setInterval會每隔10ms就去嘗試執行一次回調函數,不管上一個回調函數是不是還在執行。
從這里我們能夠學到很多,讓我們來概括一下:
- javascript引擎只有一個線程,迫使異步事件只能加入隊列去等待執行。
- 在執行異步代碼的時候,
setTimeout
和setInterval
是有着本質區別的。 - 如果計時器被正在執行的代碼阻塞了,它將會進入隊列的尾部去等待執行直到下一次可能執行的時間出現(可能超過設定的延時時間)。
- 如果interval回調函數執行需要花很長時間的話(比指定的延時長),interval有可能沒有延遲背靠背地執行。
上述這一切對於理解js引擎是如果工作的無疑是很重要的知識,尤其是大量的典型的異步事件發生時,對於構建一個高效的應用代碼片段來說是一個非常有利的基礎。
個人見解:
翻譯完成之后,感覺對於javascript異步有了新的認識,但是可能初學者看不太懂這篇文章,於是寫了一個demo,運行在nodejs環境下(瀏覽器不容易模擬)
1 var startTime = new Date(); 2 3 //初始化計時器 4 var start = setTimeout(function() { 5 var end = new Date(); 6 console.log('10ms的計時器執行完成,距離程序開始' + (end - start) + 'ms'); 7 }, 10); 8 9 //模擬鼠標點擊事件 10 function asyncReal(data, callback) { 11 process.nextTick(function() { 12 callback(); 13 }); 14 } 15 var asyncStart = new Date(); 16 asyncReal('yuanzm', function() { 17 var asyncEnd = new Date(); 18 console.log('模擬鼠標執行事件完成,花費時間' + (asyncEnd - asyncStart) + 'ms'); 19 }) 20 21 //設定定時器 22 count = 1; 23 var interval = setInterval(function() { 24 ++count; 25 if(count === 5) { 26 clearInterval(interval); 27 } 28 console.log('定時器事件'); 29 },10); 30 31 //模擬第一階段代碼執行 32 var first = []; 33 var start = new Date(); 34 for(var i = 0;i < 10000000;i++){ 35 first.push(i); 36 } 37 var end = new Date(); 38 console.log('第一階段代碼執行完成,用時' + (end - start) + 'ms');
運行結果如下:
我們按照文中的原理來解釋一下:
(1) 一開始設定的計時器並不是在10ms后立即執行,而是被添加到了隊列后面,等到第一階段代碼執行完成才執行,距離開始的時間也不是設定的10ms
(2)鼠標點擊事件同樣因為是異步事件,添加到了隊列后面,等到第一階段代碼執行完成的時候才執行。
(3)鼠標點擊事件先於計時器事件添加到隊列后面
(4)最后定時器才能執行
