淺談event loop

MoTong發表於2019-03-03

Javascript引擎是單執行緒機制,首先我們要了解Javascript語言為什麼是單執行緒

JavaScript的主要用途主要是使用者互動,和操作DOM。如果JavaScript同時有兩個執行緒,一個執行緒在某個DOM節點上新增內容,另一個執行緒刪除了這個節點,這時這兩個節點會有很大沖突,為了避免這個衝突,所以決定了它只能是單執行緒,否則會帶來很複雜的同步問題。此外HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒(UI執行緒, 非同步HTTP請求執行緒, 定時觸發器執行緒…),但是子執行緒完全受主執行緒控制,這個新標準並沒有改變JavaScript單執行緒的本質。


在瞭解event loop之前,我們先了解一下什麼是棧和佇列,他們有什麼特點?請先看兩張圖。

此處輸入圖片的描述
此處輸入圖片的描述

棧(stack) 是自動分配記憶體空間,它由系統自動釋放,特點是先進後出。
佇列的特點是先進先出。
再看一張圖:

此處輸入圖片的描述

我們程式碼執行的時候,都是在棧裡執行的,但是我呼叫多執行緒方法的時候是放到佇列裡的,先放進去的先執行。
那WebAPIs的方法什麼時候放到棧裡執行呢?
當棧裡的程式碼執行完了,會在佇列裡面讀取出來,放到棧執行。比如:寫個事件,事件裡面再呼叫非同步方法,這些方法會在呼叫的時候,放到佇列裡,會不停的迴圈。等到佇列的程式碼乾淨了,就停止迴圈了,不然就會一直迴圈。
看下面一串程式碼,會輸出什麼?

console.log(1);
setTimeout(function(){
    console.log(2)
},0)
setTimeout(function(){
    console.log(3)
},0)
console.log(`ok`);
複製程式碼

這段程式碼中,會先把setTimeout的方法移到佇列中,當棧裡的程式碼執行完之後,會把佇列裡方法取出來放到棧中執行,所以執行結果是:

1
ok
2
3
複製程式碼

再對這串程式碼進行擴充套件

console.log(1);
//A
setTimeout(function(){
    console.log(2);
    //C
    setTimeout(function(){
        console.log(4);
        //D
        setTimeout(function(){
            console.log(5);
        })
    })
},0)
//B
setTimeout(function(){
    console.log(3);
    //E
    setTimeout(function(){
        console.log(6);
    })
},0)
console.log(`ok`);
複製程式碼

這串程式碼中,棧的程式碼執行的時候,當觸發回撥函式時,會將回撥函式放到佇列中,所以,先輸出1和ok。棧裡的程式碼執行完之後,會先讀取第一個setTimeout,輸出2,這時發現裡面還有一個setTimeout(既C行下的setTimeout),這個setTimeout又會放到佇列中去。然後執行B行下的setTimeout,輸出3,這時E行下還有個setTimeout,這個setTimeout又會放到佇列中。當棧裡程式碼執行完之後,又會在佇列中讀取程式碼,這時讀取的是C行下的setTimeout,放到棧執行,輸出4,緊接著又發現D行下有setTimeout,這個setTimeout又放到佇列中排隊。棧的程式碼執行完了,又在佇列中讀取E行下的setTimeout,輸出6。執行完之後,又在佇列裡讀取D行下的setTimeout,輸出5。所以輸出結果是:

1
ok
2
3
4
6
5
複製程式碼

附圖講解:

此處輸入圖片的描述
setTiemout(function(){
    console.log(1)
},0)
for(var i = 0;i<1000;i++){
    console.log(i)
}
複製程式碼

在當前佇列裡看到setTimeout,它會等著看事件什麼時候成功。所以它會先往下走,走完以後,再把setTimeout裡的回撥函式放到佇列中。即使for迴圈的程式碼走了10s,回撥函式也會等到10s後再執行。
所以,瀏覽器的機制永遠是:先走完棧裡程式碼,才會到佇列裡去。


巨集任務和微任務

任務可分為巨集任務和微任務
巨集任務:setTimeout,setInterval,setImmediate,I/O
微任務:process.nextTick,Promise.then
佇列可以看成是一個巨集任務。

微任務是怎麼執行的?
同步程式碼先在棧中執行的,執行完之後,微任務會先執行,再執行巨集任務。
先看一個例子:

console.log(1)
setTimeout(function(){
    console.log(`setTimeout`)
},0)
let promise = new Promise(function(resolve,reject){
    console.log(3);
    resolve(100);
}).then(function(data){
    console.log(200)
})
console.log(2)
複製程式碼

想一想會輸出什麼?
程式碼由上到下執行,所以肯定先輸出1。setTimeout是巨集任務,會先放到佇列中。而new Promise是立即執行的,它是同步的,所以會先輸出3。因為then是非同步的,所以會先輸出2。因為then是微任務,微任務走完,才會走巨集任務。所以最終輸出的結果是:1 3 2 200 setTimeout。
**注意:**瀏覽器的機制是把then方法放到微任務中。
瀏覽器機制:

此處輸入圖片的描述

程式碼會先走我們的執行棧,裡面有變數,函式等等。棧的程式碼走完以後,會先去微任務,微任務裡面可能有很多回撥函式(比如:棧裡有promise的then方法,then的回撥函式會放到微任務裡去),棧裡面可能還有setTimeout,它會把setTimeout的回撥函式放到巨集任務中。什麼時候放的呢?就是當時間到達的時候,會放到佇列裡。當棧的程式碼都執行完了,它會先取微任務的then,執行。執行完之後,再取巨集任務的程式碼。(自己都快說暈了~~)

猜猜看:

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log(`ok`)
    })
})
setTimeout(function(){
    console.log(3)
})
複製程式碼

你猜輸出什麼~
分析:先預設走棧,輸出1。此時並沒有微任務,所以微任務不會執行。先走第一個setTimeout,輸出2,同時將微任務放到佇列中,執行微任務,輸出ok,微任務執行完,再走巨集任務,輸出3。

**注意:**瀏覽器和node環境輸出是不一樣的哦~

———此處是分割線————

node的event loop

接下來說說node的事件環。
先畫張圖吧

此處輸入圖片的描述

由圖可以看出微任務不在事件環裡。那程式碼怎麼走?
同樣上面的例題:

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log(`ok`)
    })
})
setTimeout(function(){
    console.log(3)
})
複製程式碼

先將2個定時器放到A中,先輸出1;這時候棧裡走完了,該走事件環了。在走事件環之前,會先將微任務清空,第一次微任務沒有東西,就濾過了。之後該走事件環了,這時候先走timers。這時候setTimeout不一定到達時間,如果到達時間,就直接執行了。如果時間沒到達,這時候可能先略過,接著往下走,走到poll輪詢階段,發現沒有讀檔案之類的操作,然後它會等著,等到setTimeout的時間到達。如果時間到達了,它會把到達時間的定時器全部執行。比如先走第一個setTimeout,並且把then方法放到微任務中。它會把到達時間的setTimeout佇列全部清掉(全部執行完),再走微任務。假如poll輪詢有很多個I/O操作,它會把I/O操作都走完,再走timers。它是一個佇列一個佇列的清空,而不是取出一個,執行一下,取出一個,執行一下。所以它會把2個setTimeout都走完,再走then。所以在node的輸出結果是:

1
2
3
ok
複製程式碼

再來個進階的栗子:

process.nextTick(function(){
    console.log(1)
})
setImmediate(function(){
    console.log(2)
})
複製程式碼

它會先走棧的內容,棧啥都沒有。當它要走事件環的時候,會將微任務清空。發現微任務有nextTick,它會把nextTick執行完,再走事件環。發現timers和poll都沒有東西,它就會走theck階段。
nextTick 和 then都是在階段轉化時才呼叫。所謂的階段轉化,就是剛開始走當前棧,在當前棧轉到timers的時候,清空微任務。

事件迴圈的順序,決定js程式碼的執行順序。進入整體程式碼(巨集任務)後,開始第一次迴圈。接著執行所有的微任務。然後再次從巨集任務開始,找到其中一個任務佇列執行完畢,再執行所有的微任務。

Node.js的Event Loop

  1. V8引擎解析JavaScript指令碼。
  2. 解析後的程式碼,呼叫Node API。
  3. libuv庫負責Node API的執行。它將不同的任務分配給不同的執行緒,形成一個Event Loop(事件迴圈),以非同步的方式將任務的執行結果返回給V8引擎。
  4. V8引擎再將結果返回給使用者。

先說到這裡吧,有欠缺的後續再補充。

相關文章