天天看點

關于JavaScript定時機制的總結

要了解JavaScript的定時機制,就要知道JavaScript的運作機制。

首先聲明,JavaScript是單線程運作(JavaScript引擎線程)事件驅動。

一、浏覽器中有多個線程

一款浏覽器中包含的最基本的線程:

1、JavaScript引擎線程。

2、定時器線程,setInterval和setTimeout會觸發這個線程。

3、浏覽器事件觸發線程,這個線程會觸發onclick、onmousemove和其它浏覽器事件。

4、界面渲染線程,負責渲染浏覽器界面HTML元素。注意:在JavaScript引擎運作腳本期間,界面渲染線程都是處于挂起狀态的。也就是說當使用JavaScript對界面中的節點進行操作時,并不會立即展現出來,要等到JavaScript引擎線程空閑時,才會展現出來。(這個最後說)

5、HTTP請求線程(Ajax請求也在其中)。

以上這些線程在浏覽器核心的控制下,互相配合,完成工作(具體我也不知道)。

二、任務隊列

我們知道JavaScript是單線程的,所有JavaScript代碼都在JavaScript引擎線程中運作。阮一峰老師的文章中叫這個線程為主線程,是一個執行棧。(以下内容也主要是根據阮一峰老師的文章了解總結。)

這些JavaScript代碼我們可以把他們看成一個個的任務,這些任務有同步任務和異步任務之分。同步任務(比如變量指派語句,alert語句,函數聲明語句等等)直接在主線程上按順序執行,異步任務(比如浏覽器事件觸發線程觸發的各種各樣的事件,使用Ajax傳回的伺服器響應等)按照時間先後順序在任務隊列(也可以叫做事件隊列、消息隊列)中排隊,等待被執行。隻要主線程上的任務執行完了,就會去檢查任務隊列,看有沒有排隊等待的任務,有就讓排隊的任務進入主線程執行。

比如下面的例子:

1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4 <meta charset="utf-8">
 5 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 6 <meta name="viewport" content="width=device-width, initial-scale=1">
 7 <title>定時機制</title>
 8 
 9 <style type="text/css">
10 body{
11    margin: 0;
12    padding: 0;
13    position: relative;
14    height: 600px;
15 }
16 
17 #test{
18    height: 30px;
19    width: 30px;
20    position: absolute;
21    left: 0;
22    top: 100px;
23    background-color: pink;
24 }
25 </style>
26 </head>
27 <body>
28    <div id="test">
29    
30    </div>
31 
32 <script>
33    var pro = document.getElementById(\'test\');
34    pro.onclick = function() {
35        alert(\'我沒有立即被執行。\');
36    };
37    function test() {
38        a = new Date();
39        var b=0;
40       for(var i=0;i<3000000000;i++) {
41          b++;
42       }
43       c = new Date();
44       console.log(c-a);
45    }
46 
47   test();
48 </script>
49 </body>
50 </html>      

在這個例子中test()函數執行完大概要8~9秒,是以當我們打開這個頁面,在8秒之前點選粉色方塊,不會立即彈出提示框,而要等到8秒之後才彈出,而且8秒之前點選幾次粉色框,8秒之後就彈出幾次。

我們打開這個頁面時,主線程先聲明函數test,再聲明變量pro,然後把p節點指派給pro,然後給p節點添加click事件,并指定回調函數(挂起),然後調用test函數,執行其中的代碼。在test函數中的代碼執行期間,我們點選了p節點,浏覽器事件觸發線程檢測到這個事件,就把這個事件放在了任務隊列中,以便主線程上的任務(這裡是test函數)執行完後,檢查任務隊列時發現這個事件并執行相應的回調函數。如果我們多次點選,這些多次觸發的事件就按觸發時間的先後在任務隊列中排隊(可以再給另外一個元素添加點選事件,交替點選不同的元素來驗證)。

下面是阮老師總結的任務的運作機制:

異步執行的運作機制如下。(同步執行也是如此,因為它可以被視為沒有異步任務的異步執行。)

1、所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。

2、主線程之外,還存在一個"任務隊列"(task queue)。隻要異步任務有了運作結果,就在"任務隊列"之中放置一個事件。

3、一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裡面有哪些事件。那些對應的異步任務,于是結束等待狀态,進入執行棧,開始執行。

4、主線程不斷重複上面的第三步。

三、事件和回調函數

 我們在給DOM元素指定事件時,都會指定一個回調函數,以便事件真的發生時執行相應的代碼。

主線程中事件的回調函數會被挂起,如果任務隊列中有正在排隊的相應的事件,當主線程檢測到時就會執行相應的回調函數。我們也可以說主線程執行異步任務,就是在執行相應的回調函數。

四、事件循環

主線程檢查任務隊列中事件的過程是循環不斷的,是以我們可以畫一個事件循環的圖:

關于JavaScript定時機制的總結

上圖中主線程産生堆和執行棧,棧中的任務執行完畢後,主線程檢查任務隊列中由其他線程傳入的發生過的事件,檢測到排在最前面的事件,就從挂起的回調函數中找出與該事件對應的回調函數,然後在執行棧中執行,這個過程一直重複。

五、定時器

結合以上知識,下面探讨JavaScript中的定時器:setTimeout()和setInterval()。

setTimeout(func, t)是逾時調用,間隔一段時間後調用函數。這個過程在事件循環中的過程如下(我的了解):

主線程執行完setTimeout(func, t);語句後,把回調函數func挂起,同時定時器線程開始計時,當計時等于t時,相當于發生了一個事件,這個事件傳入任務隊列(結束計時,隻計時一次),當主線程中的任務執行完後,主線程檢查任務隊列發現了這個事件,就執行挂起的回調函數func。我們指定的時間間隔t隻是參考值,真正的時間間隔取決于執行完setTimeout(func, t);語句後的代碼所花費的時間,而且是隻大不小。(即使我們把t設為0,也要經曆這個過程)。

setInterval(func, t)是間歇調用,每隔一段時間後調用函數。這個過程在事件循環中的過程與上面的類似,但又有所不同。

setTimeout()是經過時間t後定時器線程在任務隊列中添加一個事件(注意是一個),而setInterval()是每經過時間t(一直在計時,除非清除間歇調用)後定時器線程在任務隊列中添加一個事件,而不管之前添加的事件有沒有被主線程檢測到并執行。(實際上浏覽器是比較智能的,浏覽器在處理setInterval的時候,如果發現已任務隊列中已經有排隊的同一ID的setInterval的間歇調用事件,就直接把新來的事件 Kill 掉。也就是說任務隊列中一次隻能存在一個來自同一ID的間歇調用的事件。)

舉個例子,假如執行完setInterval(func, t);後的代碼要花費2t的時間,當2t時間過後,主線程從任務隊列中檢測到定時器線程傳入的第一個間歇調用事件,func開始執行。當第一次的func執行完畢後,第二次的間歇調用事件早已傳入任務隊列,主線程馬上檢測到第二次的間歇調用事件,func函數又立即執行。這種情況下func函數的兩次執行是連續發生的,中間沒有時間間隔。

下面是個例子:

1    function test() {
2        a = new Date();
3        var b=0;
4       for(var i=0;i<3000000000;i++) {
5          b++;
6       }
7       c = new Date();
8       console.log(c-a);
9   }
10    function test2() {
11      var d = new Date().valueOf();
12      //var e = d-a;
13      console.log(\'我被調用的時刻是:\'+d+\'ms\');
14      //alert(1);
15    }
16    setInterval(test2,3000);
17    
18   test();
      

結果:

關于JavaScript定時機制的總結

為什麼8.6秒過後沒有輸出兩個一樣的時刻,原因在上面的内容中可以找到。

執行例子中的for循環花費了8601ms,在執行for循環的過程中隊列中隻有一個間歇調用事件在排隊(原因如上所述),當8601ms過後,第一個間歇調用事件進入主線程,對于這個例子來說此時任務隊列空了,可以再次傳入間歇調用事件了,是以1477462632228ms這個時刻第二次間歇調用事件(實際上應該是第三次)傳入任務隊列,由于主線程的執行棧已經空了,是以主線程立即把對應的回調函數拿來執行,第二次調用與第一次調用之間僅僅間隔了320ms(其實8601+320=8920,差不多就等于9秒了)。我們看到第三次調用已經恢複正常了,因為此時主線程中已經沒有其他代碼了,隻有一個任務,就是隔一段時間執行一次間歇調用的回調函數。

用setTimeout()實作間歇調用的例子:

1    function test() {
 2        a = new Date();
 3        var b=0;
 4       for(var i=0;i<3000000000;i++) {
 5          b++;
 6       }
 7       c = new Date();
 8       console.log(c-a);
 9    }
10  
11    function test2(){
12       var d = new Date().valueOf();
13       console.log(\'我被調用的時刻是:\'+d+\'ms\');
14       setTimeout(test2,3000);
15    }
16    setTimeout(test2,3000);
17   test();      

結果:

關于JavaScript定時機制的總結

每兩次調用的時間間隔基本上是相同。想想為什麼?

再看一個例子:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flex布局練習</title>

<style type="text/css">
body{
   margin: 0;
   padding: 0;
   position: relative;
   height: 600px;
}

#test{
   height: 30px;
   width: 30px;
   position: absolute;
   left: 0;
   top: 100px;
   background-color: pink;
}
</style>
</head>
<body>
   <div id="test">
   
   </div>

<script>
  var p = document.createElement(\'p\');
  p.style.width = \'50px\';
  p.style.height = \'50px\';
  p.style.border = \'1px solid black\';
  
  document.body.appendChild(p);

  alert(\'ok\');
  
</script>
</body>
</html>      

這個例子的結果是提示框先彈出,然後黑色邊框的p元素才出現在頁面中。原因很簡單,就一句話:

在JavaScript引擎運作腳本期間,界面渲染線程都是處于挂起狀态的。也就是說當使用JavaScript對界面中的節點進行操作時,并不會立即展現出來,要等到JavaScript引擎線程空閑時,才會展現出來。

以上就是我對JavaScript定時機制的了解及總結,如有錯誤,希望看到的大神指正。

參考文獻:

1、JavaScript 運作機制詳解:再談Event Loop   http://www.ruanyifeng.com/blog/2014/10/event-loop.html

2、一家之言:說說 JavaScript 計時器的工作原理   http://www.daqianduan.com/1112.html

3、JavaScript可否多線程? 深入了解JavaScript定時機制   http://www.phpv.net/html/1700.html