淺析瀏覽器與nodejs中的event loop

xjylyh發表於2018-03-26

這篇文章我們來了解和簡單的分析一下瀏覽器和node環境中的event loop(事件迴圈)。

clipboard.png

有些小夥伴可能聽說過eventloop這個名詞,但是沒有了解過,接下來我們就來了解一下。

我們先來看一組程式碼。

clipboard.png

來猜一下執行結果 我們來捋一下。程式碼的執行肯定是由上自下而我們這裡有兩個定時器setTimeout,所以我們第一次執行的結果肯定是在控制檯列印出1->2->5,這個應該是毋庸置疑的,然後呢開始執行我們的setTimeout。

這裡需要注意一下這兩個setTimeout肯定也是先後順序執行的,我們應該知道在瀏覽器的執行過程中。如果不寫setTimeout的第二個引數那麼這個setTimeout的延時大約是4ms(毫秒)(我記得好像是。。).

所以當我們依次console出1、2、5之後我們的setTimeout執行,控制檯會列印 3->4。

那麼問題就來了,為什麼會輸出3->4?為什麼不是3-6-4-7或者3-4-7-6??

留個懸念。。我們直接來看這段程式碼的執行結果。

clipboard.png

node環境

clipboard.png

瀏覽器環境 我們可以看到上面的程式碼不管是node環境還是瀏覽器環境下輸出的結果是相同的,都是1-2-5-3-4-6-7.

我們來分析一下,當程式碼執行完console之後,先呼叫了第一個setTimeout然後呼叫了第二個setTimeout。

在之後又呼叫了第一個setTimeout中的setTimeout。最後執行的是第二個setTimeout中的setTimeout。

別急,我們再來看一組程式碼。

我們來看下面的程式碼。

clipboard.png

來猜一下執行結果 我們再來捋一捋。。

clipboard.png

首先我們應該肯定的是先列印1然後第一個setTimeout中列印出2 。這應該是肯定的。

然後問題又來了。。我們知道Promise是一個非同步的方法對吧。那這裡應該是先列印Promise中的then方法中的log還是下一個setTimeout中的log呢?

我們分別來看一下,程式碼在node環境和瀏覽器環境下的執行結果。

clipboard.png

node環境

clipboard.png

瀏覽器環境 我們應該注意到了。!這一次瀏覽器中的log和node環境下的log列印的順序是不一樣的。

為什麼?

這就要說到我們的event loop了。我們接下來就來說一說這個東西。

我們先來了解一下javascript的一個特點。

JavaScript語言的一大特點就是單執行緒,也就是說,同一個時間只能做一件事。那麼有些小夥伴就要問了,為什麼js不能是多執行緒的?這樣效率不是會更高嗎?

我曾經看過阮一峰老師的event loop(JavaScript 執行機制詳解:再談Event Loop),裡面是這樣解釋的。

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

所以,為了避免複雜性,從一誕生,JavaScript就是單執行緒,這已經成了這門語言的核心特徵,將來也不會改變。 為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

so,既然需求使得js這門語言是單執行緒的,那也沒辦法是不是。

接下來我們來看看瀏覽器中js執行的方式。首先我們要明白一些概念。我們來看一張圖

clipboard.png
瀏覽器環境 我們看到,這裡有以下幾個概念。

1.堆 2.執行棧 3.執行佇列

堆( heap )

什麼是堆,通俗來講堆就是存放地址的地方。比如說,堆中存放了一個一個的物件(Array object,Object object)。這些物件的指標指向的地址就是堆中存放的一個個的地址,當物件發生改變時,堆中地址的資料也會發生改變。這也就是為什麼如果修改引用型別總會影響到其他指向這個地址的引用變數。

棧( stack )

什麼是棧,再一次的通俗來講。棧存放的就是我們的一些普通的變數(基本型別)。而且棧的執行順序是先進後出。我們來模仿一下棧的執行順序(類似js中的陣列操作)。比如:

clipboard.png

我們可以看到1最先入棧最後出棧。

佇列

佇列的執行方式是先進先出的。通俗來講就像我們生活中的排隊。比如說我們排隊買票,先排隊的人肯定先買到票然後從排的隊中退出。佇列就是這樣一種效果。

瞭解過堆、棧和佇列之後我們來看張圖

clipboard.png

瀏覽器中的eventloop 我們可以看到這裡少了之前畫的圖中微任務和巨集任務的佇列的區分,其實是一樣的。

我們來分析下。

上圖中,主執行緒執行的時候,產生堆(heap)和棧(stack),棧中的程式碼呼叫各種外部API,它們在"任務佇列"中加入各種事件(click,load,done)。只要棧中的程式碼執行完畢,主執行緒就會去讀取"任務佇列",依次執行那些事件所對應的回撥函式。

我們應該想一下,既然是依次執行執行佇列的回撥函式,那為什麼之前的程式碼在瀏覽器和node中的程式碼執行結果是不一樣的呢?這就要說一說我們之前提到的微任務和巨集任務了。

微任務

Promise.then、(MutationObserve、MessageChannel)

巨集任務

setTimeout setInterval (setImmediate)

瀏覽器中任務的執行方式大約是這樣。執行棧先按執行佇列的順序執行,然後如果有微任務就執行微任務。如果沒有微任務就執行巨集任務。當巨集任務執行佇列中的一個方法執行完畢之後,執行棧會判斷微任務佇列中又沒有需要執行的微任務,如果有則執行微任務。如果沒有就執行當前執行棧中的任務。就像剛才那張圖我們再看一下。

clipboard.png

clipboard.png

也就是我們的執行棧先執行兩個setTimeout把他的回撥方法放到巨集任務佇列中。遇見Promise.then方法就把他的回撥放到微任務佇列中。然後執行棧執行完第一個setTimeout的回撥之後會判斷微任務的執行佇列中有沒有需要執行的回撥函式,當然這時我們的微任務佇列中有剛才Promise.then的回撥方法等待執行,然後我們的執行棧就會先執行微任務中的方法。當微任務執行完畢後才會回到巨集任務佇列中繼續執行。

可能有一些繞。。我們再來捋一捋看張圖

clipboard.png

額。。個人理解 我們再來看下node中的事件執行機制。

clipboard.png

node事件環 簡單的來講。就是node在執行過程中。執行棧會先執行完當前佇列才會執行下一佇列。比如我們之前的程式碼執行結果。

clipboard.png

淺析瀏覽器與nodejs中的event loop

我們可以看到,執行棧會先執行完巨集任務佇列,也就是兩個setTimeout中的回撥函式執行完才會去執行我們的微任務佇列。

所以我們看到node中的事件環和瀏覽器中的不同也就是這個樣子。

也就是說node的執行棧會先按照順序執行。然後把非同步程式碼的回撥放入到各自對應的執行佇列中。當node在處理一個執行佇列的時候不管怎樣都會先執行當前佇列,然後再去執行下一佇列。

分享這篇文章,主要是可以讓我們稍微瞭解一下在瀏覽器環境和node環境下程式碼的執行機制,也相當於我們程式碼的執行順序。

大家看到這裡也蠻不容易,謝謝大家的觀看。祝好。就醬。

clipboard.png

有興趣瞭解Promise的可以戳一下這裡:一步一步實現一個符合PromiseA+規範的Promise庫(1)

再次感謝。

相關文章