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

MRHE發表於2019-03-04

話不多說先看程式碼來引出今天的問題

//下面兩個定時器的輸出的先後順序是啥呢?
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)機制:

一:瀏覽器常駐的執行緒

  • 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. 再來看看同步任務具體執行的過程

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

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

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

  我們再來深入瞭解下執行棧:

  上面程式碼我們只套了一層函式,如果套多層函式,或者有多個bar的同級函式是有區別的。

  多層巢狀很簡單,就按照上面的流程依次內推就好了,

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

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執行一次。


五、最後的最後

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

最後打個廣告,本人男,22歲,在校大四學習。座標成都,希望能找個前端的正式崗或者實習崗工作。如果有招人或者內推的大佬,可以留言細聊喲。



 


相關文章