話不多說先看程式碼來引出今天的問題
//下面兩個定時器的輸出的先後順序是啥呢?
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執行機制
- 眾所周知 js是單執行緒的:同一時間只能做一件事。記住這個很重要,雖然上面說了3中非同步的執行緒,但是他們做的也只是把對應的事件做下處理,然後推給主執行緒來執行,而主執行緒是單執行緒的同一時間只做一件事情,多餘事情就排隊吧!!!!很重要
- 看圖說話,看看js執行流程
導圖解讀: (注意:最頂端任務進入執行棧,棧:先進後出,後進先出) js任務中無非為同步任何和非同步任務2中。在任務進入執行棧後,同步和非同步任務分別進入不同的執行“場所”,同步任務進入主執行緒,非同步任務進入Event Table 並註冊函式。當指定的事情完成時(比如:定時器的延遲時間到了,ajax請求的資料發回來了,觸發了回撥函式,dom事件被使用者觸發) ,Event Table 會將這個函式移入 Event Queue(事件佇列) 並註冊回撥函式當主執行緒的任務執行完畢後,主執行緒為空時,就會去Event Queue 看看,如果有則讀取佇列裡的函式,並將它放入主執行緒中執行(而進入Event Queue 的先後順序,也是被主執行緒抓取的順序) 。上述過程會不斷重複,這就是Event Loop (事件迴圈/事件輪循)
-
再來看看同步任務具體執行的過程
function foo(){ function bar(){ console.log("bar"); } bar(); console.log("foo"); } foo();複製程式碼
我們來具體看看上面的執行過程
- 程式碼沒有執行的時候,執行棧為空棧
- foo函式執行時,建立了一幀,這幀包含了形參、區域性變數(預編譯過程),然後把這一幀壓入棧中
- 執行foo函式內程式碼,執行bar函式
- 建立新幀,同樣有形參、區域性變數,壓入棧中
- bar函式執行完畢,輸出bar,彈出棧
- foo函式執行完畢,輸出foo,彈出棧(可能有小夥伴會說,那把console.log(”foo”)放在bar函式的執行的上面。foo函式不就先執行完了嘛? 即使這樣做了,雖然是先輸出foo但也是foo函式後執行完,因為在bar函式執行完畢後,如果後面沒有程式碼了,他會隱式的執行一句 return ; 來終止這個函式)
- 執行棧為空
我們再來深入瞭解下執行棧:
上面程式碼我們只套了一層函式,如果套多層函式,或者有多個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’);
複製程式碼
- Ajax 進入Event Table ,並註冊函式;
- ajax事件完成,http網路請求執行緒 註冊回撥函式success,並放入Event Queue(任務佇列)中等待 主執行緒(執行棧)讀取任務
- 主執行緒讀取 success函式並執行,console.log(data);
5.換一張圖繼續理解
對2 做一點補充:
細心的朋友已經發行,我在上面寫 主執行緒的時候()裡面寫了一個呼叫棧。沒錯 執行棧其實相當於js主執行緒。我的個人理解,js單執行緒執行是,遇到同步的程式碼,從上到下依次(預編譯的問題另說),遇到非同步的程式碼就一腳踢開,讓該管非同步程式碼的去管理(參考第一點瀏覽器常駐執行緒)。等同步程式碼執行完畢之後,再去看看Event Queue(任務佇列)裡面看看有沒有,可以執行的程式碼(回撥,定時器,事件),有就拿過來執行,沒有就一會再來看看(這個事件特別短,也可能是有專門的觸發機制,總的就是 只有執行棧為空,Event Queue裡面有任務就會馬上拿來執行)
三:問題的解決
好了,說到這裡,就可以回頭來看看我們最開始丟擲的問題:
對上面程式碼的分析:
- 遇到setTimeout(fn,200) 一腳踢開,讓定時器觸發執行緒去管理,在一邊面壁思過的數數,數夠了200ms,就推入Event Queue中;
- for迴圈 ,就一直執行,直到執行完畢再往下走
- 遇到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歲,在校大四學習。座標成都,希望能找個前端的正式崗或者實習崗工作。如果有招人或者內推的大佬,可以留言細聊喲。