Node.js Event Loop與瀏覽器 Event Loop(事件環)

張文夢發表於2018-06-07

要理解Event Loop首先要學習幾個概念,下面就通過這些概念來一步步解析Event Loop

一:程式與執行緒

程式是作業系統分配資源和排程任務的基本單位,執行緒是建立在程式上的一次程式執行單位,一個程式上可以有多個執行緒。

1.1 為什麼JavaScript是單執行緒?

JavaScript語言的一大特點就是單執行緒,也就是說,同一個時間只能做一件事。那麼,為什麼JavaScript不能有多個執行緒呢?這樣能提高效率啊。

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

所以,為了避免複雜性,從一誕生,JavaScript就是單執行緒,這已經成了這門語言的核心特徵,將來也不會改變。

為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

這裡所謂的單執行緒指的是主執行緒是單執行緒的,所以在Node中主執行緒依舊是單執行緒的。

1.2 其他執行緒

  • 瀏覽器事件觸發執行緒(用來控制事件迴圈,存放setTimeout、瀏覽器事件、ajax的回撥函式)
  • 定時觸發器執行緒(setTimeout定時器所線上程)
  • 非同步HTTP請求執行緒(ajax請求執行緒)

單執行緒特點是節約了記憶體,並且不需要在切換執行上下文。而且單執行緒不需要管鎖的問題,這裡簡單說下鎖的概念。例如下課了大家都要去上廁所,廁所就一個,相當於所有人都要訪問同一個資源。那麼先進去的就要上鎖。而對於node來說。下課了就一個人去廁所,所以免除了鎖的問題!

二:佇列和棧

2.1 棧記憶體or堆記憶體

堆和棧這兩個字我們已經接觸多很多次,那麼具體是什麼存在棧中什麼存在堆中呢?就拿JavaScript中的變數來說:

  • 首先JavaScript中的變數分為基本型別和引用型別。
  • 基本型別就是儲存在棧記憶體中的簡單資料段,而引用型別指的是那些儲存在堆記憶體中的物件。

1、基本型別

基本型別有Undefined、Null、Boolean、Number 和String。這些型別在記憶體中分別佔有固定大小的空間,他們的值儲存在棧空間,我們通過按值來訪問的。

2、引用型別

引用型別,值大小不固定,棧記憶體中存放地址指向堆記憶體中的物件。是按引用訪問的。如下圖所示:棧記憶體中存放的只是該物件的訪問地址,在堆記憶體中為這個值分配空間。由於這種值的大小不固定,因此不能把它們儲存到棧記憶體中。但記憶體地址大小的固定的,因此可以將記憶體地址儲存在棧記憶體中。 這樣,當查詢引用型別的變數時, 先從棧中讀取記憶體地址, 然後再通過地址找到堆中的值。對於這種,我們把它叫做按引用訪問。

Node.js Event Loop與瀏覽器 Event Loop(事件環)

2.2 任務佇列

單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。

如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閒著的,因為IO裝置(輸入輸出裝置)很慢(比如Ajax操作從網路讀取資料),不得不等著結果出來,再往下執行。

JavaScript語言的設計者意識到,這時主執行緒完全可以不管IO裝置,掛起處於等待中的任務,先執行排在後面的任務。等到IO裝置返回了結果,再回過頭,把掛起的任務繼續執行下去。

於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

具體來說,非同步執行的執行機制如下。(同步執行也是如此,因為它可以被視為沒有非同步任務的非同步執行。)

 (1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
 (2)主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
 (3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
 (4)主執行緒不斷重複上面的第三步。
複製程式碼

三:瀏覽器中的Event Loop

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

為了更好地理解Event Loop,請看下圖

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

  • 所有同步任務都在主執行緒上執行,形成一個執行棧
  • 主執行緒之外,還存在一個任務佇列。只要非同步任務有了執行結果,就在任務佇列之中放置一個事件。
  • 一旦執行棧中的所有同步任務執行完畢,系統就會讀取任務佇列,將佇列中的事件放到執行棧中依次執行
  • 主執行緒從任務佇列中讀取事件,這個過程是迴圈不斷的

整個的這種執行機制又稱為Event Loop(事件迴圈)

四:Node.js的Event Loop

Node.js也是單執行緒的Event Loop,但是它的執行機制不同於瀏覽器環境。

請看下面的示意圖

Node.js Event Loop與瀏覽器 Event Loop(事件環)
根據上圖,Node.js的執行機制如下。

  • 我們寫的js程式碼會交給v8引擎進行處理
  • 程式碼中可能會呼叫nodeApi,node會交給libuv庫處理
  • libuv庫負責Node API的執行。它將不同的任務分配給不同的執行緒,形成一個Event Loop(事件迴圈),以非同步的方式將任務的執行結果返回給V8引擎。
  • 通過事件驅動的方式,將結果放到事件佇列中,最終交給我們的應用。

除了setTimeout和setInterval這兩個方法,Node.js還提供了另外兩個與"任務佇列"有關的方法:process.nextTick和setImmediate。

process.nextTick方法可以在當前"執行棧"的尾部----下一次Event Loop(主執行緒讀取"任務佇列")之前----觸發回撥函式。也就是說,它指定的任務總是發生在所有非同步任務之前。setImmediate方法則是在當前"任務佇列"的尾部新增事件,也就是說,它指定的任務總是在下一次Event Loop時執行,這與setTimeout(fn, 0)很像

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
複製程式碼

上面程式碼中,由於process.nextTick方法指定的回撥函式,總是在當前"執行棧"的尾部觸發,所以不僅函式A比setTimeout指定的回撥函式timeout先執行,而且函式B也比timeout先執行。這說明,如果有多個process.nextTick語句(不管它們是否巢狀),將全部在當前"執行棧"執行。

相關文章