首頁  >  文章  >  web前端  >  深入理解JavaScript定時機制

深入理解JavaScript定時機制

高洛峰
高洛峰原創
2016-12-08 15:14:241336瀏覽

本文介紹了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返回的伺服器回應等等)依照時間先後順序在任務佇列(也可以稱為事件佇列、訊息佇列)中排隊,等待被執行。只要主執行緒上的任務執行完了,就會去檢查任務佇列,看有沒有排隊等待的任務,有就讓排隊的任務進入主執行緒執行。

例如下面的例子:

<!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>定时机制</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 pro = document.getElementById(&#39;test&#39;);
  pro.onclick = function() {
    alert(&#39;我没有立即被执行。&#39;);
  };
  function test() {
    a = new Date();
    var b=0;
   for(var i=0;i<3000000000;i++) {
     b++;
   }
   c = new Date();
   console.log(c-a);
  }
 
 test();
</script>
</body>
</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函数的两次执行是连续发生的,中间没有时间间隔。

下面是个例子:

function test() {
    a = new Date();
    var b=0;
   for(var i=0;i<3000000000;i++) {
     b++;
   }
   c = new Date();
   console.log(c-a);
 }
  function test2() {
   var d = new Date().valueOf();
   //var e = d-a;
   console.log(&#39;我被调用的时刻是:&#39;+d+&#39;ms&#39;);
   //alert(1);
  }
  setInterval(test2,3000);
   
 test();

   

结果:

深入理解JavaScript定時機制

为什么8.6秒过后没有输出两个一样的时刻,原因在上面的内容中可以找到。

执行例子中的for循环花费了8601ms,在执行for循环的过程中队列中只有一个间歇调用事件在排队(原因如上所述),当8601ms过后,第一个间歇调用事件进入主线程,对于这个例子来说此时任务队列空了,可以再次传入间歇调用事件了,所以1477462632228ms这个时刻第二次间歇调用事件(实际上应该是第三次)传入任务队列,由于主线程的执行栈已经空了,所以主线程立即把对应的回调函数拿来执行,第二次调用与第一次调用之间仅仅间隔了320ms(其实8601+320=8920,差不多就等于9秒了)。我们看到第三次调用已经恢复正常了,因为此时主线程中已经没有其他代码了,只有一个任务,就是隔一段时间执行一次间歇调用的回调函数。

用setTimeout()实现间歇调用的例子:

function test() {
    a = new Date();
    var b=0;
   for(var i=0;i<3000000000;i++) {
     b++;
   }
   c = new Date();
   console.log(c-a);
  }
  
  function test2(){
   var d = new Date().valueOf();
   console.log(&#39;我被调用的时刻是:&#39;+d+&#39;ms&#39;);
   setTimeout(test2,3000);
  }
  setTimeout(test2,3000);
 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(&#39;p&#39;);
 p.style.width = &#39;50px&#39;;
 p.style.height = &#39;50px&#39;;
 p.style.border = &#39;1px solid black&#39;;
  
 document.body.appendChild(p);
 
 alert(&#39;ok&#39;);
  
</script>
</body>
</html>

   

这个例子的结果是提示框先弹出,然后黑色边框的p元素才出现在页面中。原因很简单,就一句话:

在JavaScript引擎运行脚本期间,界面渲染线程都是处于挂起状态的。也就是说当使用JavaScript对界面中的节点进行操作时,并不会立即体现出来,要等到JavaScript引擎线程空闲时,才会体现出来。


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn