JavaScript的計時器的工作原理


最近都在看一些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)最后定時器才能執行


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM