[JS]你不知道的Event Loop

從一道面試題說起

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

console.log(222);

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

JS的特點

JS的單線程

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

  1. 爲了提高效率,減少CPU的開銷。在多線程中,CPU需要來回切換線程,就會存在線程切換上的開銷。
  2. JS最初設計時,是作爲瀏覽器的腳本語言,主要用途是與用戶互動,以及操作DOM。這就決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JS同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程爲準?

JS的異步

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

任務隊列

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

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

在JS中,所有任務可以分成兩種。一種是同步任務,另一種是異步任務。
同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程,而進入"任務隊列"的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。

任務隊列中的任務事件,一般有個共性就是存在"回調函數"。所謂"回調函數",就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務時,執行就是對應的回調函數。

值得一提的是,任務隊列不止一條。由於異步任務有很多種,比如事件監聽類,定時器類,Ajax請求類...所以可以有很多條任務隊列。這樣說大家可能還不太明白,我畫個圖解釋下

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引擎中其實是這樣執行的

var a = 11111
console.log(a)
var btn1 = document.getElementById('btn1')
var btn2 = document.getElementById('btn2')
console.log(55555)

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

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語言與衆不同的運行方式,也因此使它具備了其他語言不具備的優勢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章