深度剖析定時器、提一嘴事件輪循

話很少說先看代碼來引出今天的問題javascript

//下面兩個定時器的輸出的前後順序是啥呢?
setTimeout(function(){            
    console.log("200")        
},200)  
//不瞭解ES6的朋友,把let 當成var 就好 
for(let i = 0 ; i < 1000 ; i++){            
    console.log('---');        
}        
setTimeout(function(){            
    console.log('0')   
    //實際不可能會是0ms,定時器有一個最低的延時爲4ms,形成這個的緣由,我相信聰明的你,
    //確定能在下面的世界輪循機制中找到答案(定時器觸發線程和主線程的取出,會有必定的執行時間) 
},0)

//而下面兩個定時器的輸出結果又是啥呢?
setTimeout(function(){
    console.log("200")
},200)
for(let i= 0; i < 5000 ; i++){
    console.log("---");
}

setTimeout(function(){
    console.log("0")
},0)

複製代碼
//上面兩個的答案分別是 0  200;    200 0 複製代碼

。那麼問題來了,第二個定時的delay(延遲時間,如下都用這個單詞表示了)明明是 0ms(實際大約4ms,代碼中解釋了,下面再也不作解釋)。第一個定時器的delay 是 200ms,爲啥第一個代碼正常輸出,而第二個代碼確實 delay爲200ms 的先輸出?
前端

如今帶着咱們的問題來看看js的事件輪循(Event Loop)機制:java

一:瀏覽器常駐的線程

  • js引擎線程(解釋執行js代碼、用戶輸入、網絡請求)
  • GUI線程(繪製用戶界面與JS主線程是互斥的。 幹了其中一個就不能作另一個)
  • http網絡請求線程(處理用戶的GET、POST等請求,等返回結果後將回調函數推入任務隊列(Evnet Queue))
  • 定時器觸發器線程(setTimeout、setInterval等待時間結束後把執行函數推入任務隊列中)
  • 瀏覽器事件處理線程(將click、mouse等交互事件發生後將這些事件放入執行隊列中)

二:js執行機制

  1.     衆所周知 js是單線程的:同一時間只能作一件事。記住這個很重要,雖然上面說了3中異步的線程,可是他們作的也只是把對應的事件作下處理,而後推給主線程來執行,而主線程是單線程的同一時間只作一件事情,多餘事情就排隊吧!!!!很重要
  2.     看圖說話,看看js執行流程
    導圖解讀: (注意:最頂端任務進入執行棧,棧:先進後出,後進先出)                                 js任務中無非爲同步任何和異步任務2中。在任務進入執行棧後,同步和異步任務分別進入不一樣的執行「場所」,同步任務進入主線程,異步任務進入Event Table 並註冊函數。
    當指定的事情完成時(好比:定時器的延遲時間到了,ajax請求的數據發回來了,觸發了回調函數,dom事件被用戶觸發) ,Event Table 會將這個函數移入 Event Queue(事件隊列) 並註冊回調函數
    當主線程的任務執行完畢後, 主線程爲空時,就會去Event Queue 看看,若是有則讀取隊列裏的函數,並將它放入主線程中執行(而進入Event Queue 的前後順序,也是被主線程抓取的順序) 。上述過程會不斷重複,這就是 Event Loop (事件循環/事件輪循)
  3. 再來看看同步任務具體執行的過程ajax

    function foo(){ 
       function bar(){
     console.log("bar");   }
       bar();
       console.log("foo");
    }
    foo();複製代碼

      咱們來具體看看上面的執行過程json

  1. 代碼沒有執行的時候,執行棧爲空棧
  2. foo函數執行時,建立了一幀,這幀包含了形參、局部變量(預編譯過程),而後把這一幀壓入棧中
  3. 執行foo函數內代碼,執行bar函數
  4. 建立新幀,一樣有形參、局部變量,壓入棧中
  5. bar函數執行完畢,輸出bar,彈出棧
  6. foo函數執行完畢,輸出foo,彈出棧(可能有小夥伴會說,那把console.log("foo")放在bar函數的執行的上面。foo函數不就先執行完了嘛? 即便這樣作了,雖然是先輸出foo但也是foo函數後執行完,由於在bar函數執行完畢後,若是後面沒有代碼了,他會隱式的執行一句  return ; 來終止這個函數)
  7. 執行棧爲空

  咱們再來深刻了解下執行棧:瀏覽器

  上面代碼咱們只套了一層函數,若是套多層函數,或者有多個bar的同級函數是有區別的。bash

  多層嵌套很簡單,就按照上面的流程依次內推就行了,網絡

  同級函數則是是重複 3,4,5的步驟。bar執行完畢,彈出棧,bar後面的代碼繼續執行碰到函數執行則走3,4,5,步驟。dom

4.異步任務具體的執行過程異步

$.ajax({
	url: ‘localhost:/js/demo.json’,
	data: {},
	success: function (data) {
		console.log(data);
	}
});
console.log(‘run’);
複製代碼

  1. Ajax 進入Event Table ,並註冊函數;
  2. ajax事件完成,http網絡請求線程 註冊回調函數success,並放入Event Queue(任務隊列)中等待 主線程(執行棧)讀取任務
  3. 主線程讀取 success函數並執行,console.log(data);

5.換一張圖繼續理解


 對2 作一點補充:

 細心的朋友已經發行,我在上面寫 主線程的時候()裏面寫了一個調用棧。沒錯 執行棧其實至關於js主線程。個人我的理解,js單線程執行是,遇到同步的代碼,從上到下依次(預編譯的問題另說),遇到異步的代碼就一腳踢開,讓該管異步代碼的去管理(參考第一點瀏覽器常駐線程)。等同步代碼執行完畢以後,再去看看Event Queue(任務隊列)裏面看看有沒有,能夠執行的代碼(回調,定時器,事件),有就拿過來執行,沒有就一會再來看看(這個事件特別短,也多是有專門的觸發機制,總的就是 只有執行棧爲空,Event Queue裏面有任務就會立刻拿來執行

三:問題的解決

好了,說到這裏,就能夠回頭來看看咱們最開始拋出的問題:

對上面代碼的分析:

  1.  遇到setTimeout(fn,200) 一腳踢開,讓定時器觸發線程去管理,在一邊面壁思過的數數,數夠了200ms,就推入Event Queue中;
  2. for循環 ,就一直執行,直到執行完畢再往下走
  3. 遇到setTimeout(fn,0) 一腳踢開讓,定時器觸發線程去管理,在一邊面壁思過的數數,數夠了200ms,就推入Event Queue中;

 由上面的文字能夠分析出,只要for循環的執行時間超過了200ms,第一個定時器就先進入Event Queue中(任務隊列,先進先出,後進後出。先進去的就先執行),第二個定時器是在第一個定時器已經進入了Event Queue 以後再觸發的,無論他的delay多小也只有後輸出。

而for循環的執行時間沒有超過200ms時(低於先觸發的定時器的delay),for循環執行完畢後,他還在面壁思過的數數,js主線程繼續往下走,觸發了第二個定時器,依舊一腳踢開,去面壁思過數數,這個時候,只要誰先數完,誰就先進入Event Queue 就先執行 。 上面代碼的狀況是 delay 爲0ms 的先數完,因此先執行,delay爲200ms後進入Event Queue 後執行。

四:問題加深

 你覺得這樣就完了嗎?若是是這樣敢說深度剖析定時器?看代碼

//表示執行次數的變量        
let count = 0;        /
/開始時間,用來定時的,記錄執行的間隔時間        
// + 爲一元 '+' 號運算符,將其操做數隱式轉換成數字         
let starTime = +new Date();        
function sleep (num){            
    for(let i = 0 ;i < num ; i++){                
    console.log(i);            
    }        
}        
setInterval(function(){            
    count++;            
    console.log(+new Date()  - starTime , count);            
    starTime = +new Date();           
    },1000)  
              
sleep(20000);複製代碼

先上執行結果


上面的執行結果除了第二次的都很好解釋。第一次執行,時間這麼多的緣由是,運行for循環完了以後才能執行定一次的定時器,3以後的就趨於穩定 大概等於delay。

先拋出問題:

    首先主線程一直在運行的時候,setInterval是每到一個delay就往Event Queue推出一個執行函數嗎?若是是這樣的話,如圖所示第一次執行被阻塞的時候爲3000 + ,因此能往Evnet Queue裏面註冊三個定時器,爲啥只有第二次的執行間隔時間發生比較大的差距,第三次之後就正常了?  爲何 第一次和第二次執行的間隔時間相加總約等於delay的倍數,這是巧合仍是必然?


回答問題:

   咱們先定義一些參數,好方便如下的解釋:

   fn1 爲定時器的第一次  , fn 2  爲定時器的第二次 , fn3 爲定時器 第三次和之後的無限次

 關於上面的第一個問題很容易回答, setInterval 確定不是沒到一個delay就往Event Queue 推送一個執行函數 ,若是是的話如上代碼就會有三個執行函數在任務隊列裏面了,當主線程執行完畢後,去Event Queue拿函數回去執行會很是快,不可能會出現,fn2,fn3執行間隔這麼大。 其實第三個問題纔是解題的關鍵,是仔細想想,什麼狀況下才能出現這種相加爲倍數的狀況(好吧,其實怎麼想,我也說不清楚)。在試驗的過程當中,甚至出現過fn1 的執行間隔爲3950 ,fn2的執行間隔爲49的狀況,當時確實給我形成了很大的悟道,後面經過不斷的實驗,加詢問最終得出告終論


 解決:出現這個事情的緣由是,Event Queue 裏面只能存在同一個定時器的一次事件,也就是說在定時器第一次被拿到主線程取走以前,第二次並不會進入Event Queue 。會依舊再Event Table 裏面等待。這個等待並非盲目的等待,在每個delay週期都看看Event Queue 裏面  上一次 的進去的定時器(fn1) 被主線程取走沒有,當取走後,就會在當前delay週期完的時候,把這一次的定時器(fn2)推入 Event Queue ,而這個時候主線程正好沒有任務正在執行,主線程就會馬上把此次的定時器放入到主線程執行,就形成了,定時器第一次執行和第二次執行的間隔時間相加總等於delay的倍數。 fn3以後的就屬於正常狀況了,當主線程沒有任務,Event Queue 中沒有定時器時,就每隔delay執行一次。


5、最後的最後

  第一次寫掘金文章(也是第一次寫文章),清辯證看待,其中的一些錯別字和錯誤。若是對你有幫助,別忘了點個贊喲。

最後打個廣告,本人男,22歲,在校大四學習。座標成都,但願能找個前端的正式崗或者實習崗工做。若是有招人或者內推的大佬,能夠留言細聊喲。

相關文章
相關標籤/搜索