JavaScript與Event Loop

chenwen666發表於2018-03-25

看標題,我想大家應該猜到我要說什麼了,在我在談論這個話題之前首先需要先說明一點:接下來的描述可能會存在錯誤,和許多我自己的觀點。如有不對的地方還請大家幫忙指正。

說到event loop,首先先來說一說JavaScript的執行機制。

JavaScript的執行機制

JavaScript是一門主執行緒是單執行緒的語言。為什麼會這樣規定呢?因為如果多個執行緒同時做一件事那豈不很混亂,比如一個執行緒要刪除a,另一個執行緒在同樣時間要修改a,那應該執行誰呢?為此,為了簡化操作,JavaScript的主執行緒設定為單執行緒。

這樣不難猜出JavaScript在執行程式碼的時候是自上而下的執行程式碼,當我寫入一段程式碼時,執行的順序應該是先後順序的。看一段程式碼:

//javascript
console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve().then(function(){
        console.log(5);
    })
},0);
Promise.resolve().then(function(){
    console.log(3)
    
})
console.log(4);
複製程式碼

按照我們剛剛推理的,輸出的結果順序應該是1->2->3->4。然而,並不是。那這是為什麼呢?這一段簡簡單單的程式碼就帶來了很多訊息。

這段程式碼遵循的是同步程式碼先執行,非同步程式碼後執行,且程式碼都會放入棧中執行。這個地方就出現了一些專業術語,什麼是同步?什麼是非同步?什麼是棧呢?

堆和棧

JavaScript中有棧和堆。比較片面理解的話,其實可以這樣說,堆就是用來儲存複雜的,比如物件(Object),而棧可以叫執行棧,用來執行程式碼的。棧(stack) 是自動分配記憶體空間,它由系統自動釋放; 堆(heap) 則是動態分配的記憶體,大小不定也不會自動釋放,堆裡面存放的是物件或者陣列物件。

同步和非同步

其實在理解同步和非同步這倆詞的時候我覺得舉例子更能說清楚一些。舉例:我燒開水從熱水一直等到水燒開,中途沒有做其他任何事,只是等水燒開。這個過程就是同步。如果我在水還沒燒開的同時,把水壺洗乾淨,裡面倒上茶,水燒開後將開水倒入水壺中。這個過程就是非同步。

同步: 指的是被呼叫者在執行任務等到完成後返回結果

非同步: 指的是被呼叫者在執行任務時,過一段時間再返回結果。

瀏覽器的event loop

其實上述程式碼中,遵循的是一個不斷迴圈的過程(如下圖所示),在這個迴圈中,所有的程式碼將會在棧中執行,每一次迴圈都會將巨集任務依次排列到 佇列(queue) 中(比如:setTimeout),並檢查是否有微任務,如果有,先將微任務的程式碼執行,在執行巨集任務中的程式碼。

JavaScript與Event Loop

因此我們再來重新看上面的程式碼:同步先輸出來就有1,4,緊接著到microtask裡面的promise.then(),輸出3,最後輸出macrotask裡的setTimeout()的值2,順序就是1->4->3->2。

node.js的Event Loop

node.js簡介

node.js是基於Chrome v8引擎的JavaScript執行環境,使用了事件驅動,非阻塞I/O的模型。node.js的特點是非同步且主執行緒也是單執行緒。

node.js的優點:

佔用資源小,因為是單執行緒,在大負荷情況下,對記憶體佔用仍然很低;
執行緒安全,沒有加鎖、解鎖、死鎖這些問題。

node.js Event Loop

node.js的底層也是多個執行緒,阻塞操作封裝的。node.js裡由6個階段來執行event loop的,可以查閱資料點這裡,這裡就不copy了。現在我們主要敘述node的event loop與瀏覽器的event loop的區別。

瀏覽器與node.js兩者的event loop

根據兩者的event loop的執行機制,他們在執行程式碼的執行順序是存在差異的。 來看一個例子:

//javascript
console.log(1);
setTimeout(function(){
    console.log('setTimeout1');
    process.nextTick(function(){
        console.log('nextTick1');
    })
},0);
process.nextTick(function(){
    console.log('nextTick2');
    setTimeout(function(){
        console.log(setTimeout2);
    })
})
console.log(2);
複製程式碼

瀏覽器執行的過程

在瀏覽器中執行process的程式碼,需要在伺服器上運,process屬於node.js的語法。

1.第一遍執行,從上往下走,遇到同步程式碼,輸出1;遇到setTimeout,排入佇列第一個;再往下走,遇到nextTick(),排入微任務第一個,再往下走,遇到同步程式碼,輸出2。第一遍迴圈:輸出1->2

2.第二遍執行,取出第一遍迴圈中第一次排入微任務中的nextTick,輸出nextTick2,往下執行時,發現還有setTimeout,將其排入佇列中。微任務走完,到巨集任務程式碼,輸出setTimeout1,往下走時,發現一個process.nextTick(),放入微任務中。第二遍迴圈:輸出nextTick2->setTimeout1

3.第三遍執行,只剩下微任務中一個process.nextTick(function(){console.log('nextTick1')})和佇列中的setTimeout(function(){console.log('setTimeout2')}),先執行微任務,輸出promise2,再執行巨集任務,輸出setTimeout1。第三遍迴圈:輸出nextTick1->setTimeout2

最後輸出的順序應該是:1->2->nextTick2->setTimeout1->nextTick1->setTimeout2

node.js執行的過程

node.js執行的過程遵循它的六個階段,如圖(圖片是借鑑的):

每個階段都有自己的callback佇列,每當進入某個階段,都會從所屬的佇列中取出callback來執行,當佇列為空或者被執行callback的數量達到系統的最大數量時,進入下一階段。這六個階段都執行完畢稱為一輪迴圈。

JavaScript與Event Loop
在node.js中也存在微任務(microtask),process.nextTick()可以看成是一個微任務。只是它執行的順序與瀏覽器的不同。node.js中,微任務是在階段轉化時,才會執行

我們再來看上面那段程式碼的執行順序。

第一遍執行:程式碼自上而下執行,遇到同步程式碼,先輸出1,發現setTimeout,放入對應的node.js的階段中(timers),往下執行,發現nextTick,放入微任務中;再往下執行,還有同步程式碼,輸出2。第一遍迴圈:輸出1->2

第二遍執行:取出第一遍執行的微任務nextTick,輸出nextTick2,往下執行,發現有setTimeout,將其放入對應的階段中(timers)。程式碼再往下執行,發現有nextTick,放入微任務中。這時node.js的 timers階段會有兩個setTimeout,它會先把階段中的程式碼執行完,再執行微任務。因此,接下來輸出的是第一遍執行時的setTimeout1,發現這個階段還未執行完,緊接著輸出setTimeout2.最後階段轉換,發現微任務,輸出nextTick1。此時輸出的順序是:nextTick2->setTimeout1->setTimeout2->nextTick1

node.js中還有一些比較有趣的地方,這裡就不講述為什麼了。只是簡單列舉出來

  • process.nextTick()會優先於Promise.then()執行
  • setImmdieate()和setTimeout()順序不確定

小板凳坐等,歡迎各位大佬來撩!

JavaScript與Event Loop

參考資料

cnode.js 不要混淆nodejs和瀏覽器中的event loop

event loop的規範實現

相關文章