『前端乾貨篇』: 你不知道的Event Loop

醬菜豪發表於2018-10-29

一星期的滿課,身心疲憊(×_×)...週末閒下來,仔細研究了下JS的事件輪詢機制,看了看阮一峰大大的相關文章,真的收貨挺多。

從一道面試題說起

setTimeout(function() {
  console.log(111);
}, 0);   // 這裡定時器時間設定為0ms後執行

console.log(222);
複製程式碼

相信這道題很多人都看過,結果是先輸出222,再輸出111
可能新手會犯錯,認為定時器設定0毫秒就等於立即就執行,所以先輸出111。但其實內部涉及一個很重要的JS執行機制,也就是我們今天的主角——事件輪詢(Event Loop)

JS的特點

在聊Event Loop之前,有必要先講講JS的一些重要特點

JS的單執行緒

JS的一大特點就是單執行緒,也就是說,同一個時間只能做一件事。那麼,為什麼JS不能有多個執行緒呢?

第一,為了提高效率,減少CPU的開銷。在多執行緒中,CPU需要來回切換執行緒,就會存線上程切換上的開銷。

第二,JS最初設計時,是作為瀏覽器的指令碼語言,主要用途是與使用者互動,以及操作DOM。這就決定了它只能是單執行緒,否則會帶來很複雜的同步問題。比如,假定JS同時有兩個執行緒,一個執行緒在某個DOM節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?

JS的非同步

說到JS的非同步,可能有同學會問啦,JS是單執行緒的怎麼還能非同步執行,這不是自相矛盾嗎?的確,單執行緒和非同步確實不能同時成為一個語言的特性,所以它本身不可能是非同步的。一定是存在一種機制讓它能夠非同步執行,往下看!

任務佇列

JS是單執行緒就意味著,所有任務需要排隊,等前一個任務結束,才能執行後一個任務。但前端的某些任務是非常耗時的,例如IO裝置(輸入輸出裝置)、Ajax操作(從網路讀取資料)、定時器...不得不等著結果出來,再往下執行。如果讓他們和別的任務一樣,都老老實實的排隊等待執行的話,執行效率會非常的低,甚至導致頁面的假死,使用者體驗很差。

這個時候,任務佇列就派上用場了。

在JS中,所有任務可以分成兩種。一種是同步任務,另一種是非同步任務。

同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒,而進入"任務佇列"的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

任務佇列中的任務事件,一般有個共性就是存在"回撥函式"。所謂"回撥函式",就是那些會被主執行緒掛起來的程式碼。非同步任務必須指定回撥函式,當主執行緒開始執行非同步任務時,執行就是對應的回撥函式。

值得一提的是,任務佇列不止一條。由於非同步任務有很多種,比如事件監聽類,定時器類,Ajax請求類...所以可以有很多條任務佇列

這樣說大家可能還不太明白,我畫個圖解釋下

『前端乾貨篇』: 你不知道的Event Loop

Event Loop

主執行緒從"任務佇列"中讀取事件,這個過程是迴圈不斷的,所以整個的這種執行機制又稱為Event Loop(事件輪詢)。
執行流程

(1)所有同步任務都在主執行緒上執行,形成一個執行棧(每執行一條程式碼,向棧中壓入這條程式碼)。

(2)主執行緒之外,還存在一個"任務佇列"。存放非同步執行的程式碼,如定時器、事件監聽回撥函式等,進入等待狀態。

(3)一旦主執行緒中的所有同步任務執行完畢,就會讀取"任務佇列",看看裡面有哪些任務。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。

(4)主執行緒不斷重複上面的第三步(輪詢)。
複製程式碼

具體舉個例子吧
假如我們有一段程式碼

var a = 11111
console.log(a)

var btn1 = document.getElementById('btn1')
btn1.onclick = function() {
    console.log(22222)
}

var btn2 = document.getElementById('btn2')
btn2.onclick = function() {
    console.log(33333)
}

setTimeout(function() {
  console.log(44444)
}, 1000)

console.log(55555)
複製程式碼

以上程式碼在JS引擎中其實是這樣執行的

『前端乾貨篇』: 你不知道的Event Loop

var a = 11111
console.log(a)
var btn1 = document.getElementById('btn1')
var btn2 = document.getElementById('btn2')
console.log(44444)
複製程式碼

這五句程式碼是同步程式碼,會直接進入主執行緒,依次執行

btn.onclick = function() {
    console.log(22222)
}

btn2.onclick = function() {
    console.log(33333)
}

setTimeout(function() {
  console.log(33333)
}, 1000)
複製程式碼

這三塊非同步程式碼不會直接進入主執行緒,而是先在相應的任務佇列中註冊

當主執行緒執行完所有同步程式碼時,就開始不斷輪詢任務佇列是否有任務需要執行,輪詢的過程很快。在輪詢過程中,要是使用者點選了btn1按鈕,任務佇列會通知主執行緒,"說我這有非同步程式碼已就緒,需要你來執行"。這時btn1.onclick就從任務佇列中彈出,到主執行緒中執行

同樣的,當過了1s時,任務佇列會通知定時器需要執行,這時主執行緒輪詢時得到這條"通知",所以就執行定時器中語句

知道這個機制後,我們再回頭看看那個面試題

setTimeout(function() {
  console.log(111);
}, 0);   // 這裡定時器時間設定為0ms後執行

console.log(222);
複製程式碼

這裡的console.log(222) 首先在主執行緒中執行,而定時器則是先在任務佇列中註冊。當主執行緒中程式碼執行完(也就是console.log('222')這條語句執行完後),主執行緒開始輪詢任務佇列中的非同步程式碼,由於定時器設定的時間是0ms,所以任務佇列會立即通知主執行緒,可以執行。最後定時器就會到主執行緒中開始執行。這就是為什麼列印的結果先是222,後111。

總結

JS的事件輪詢的機制,使任務佇列、JS主執行緒、非同步操作之間可以相互協作。這正是JS語言與眾不同的執行方式,也因此使它具備了其他語言不具備的優勢。
最後感謝大家百忙之中辛苦觀看,也希望這篇文章可以幫助螢幕前的你更好的理解JS的Event Loop機制!

相關文章