Node中的事件迴圈

NoStubborn發表於2018-07-19

事件迴圈中的各階段

Node.js 的事件迴圈流程大致如下:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
複製程式碼

每個階段都有自己的任務佇列,當本階段的任務佇列都執行完畢,或者達到了執行的最大任務數,就會進入到下一個階段。

timers 階段

這個階段會執行被 setTimeoutsetInterval 設定的定時任務。 當然,這個定時並不是準確的,而是在超過了定時時間後,一旦得到執行機會,就立刻執行。

pending callbacks 階段

這個階段會執行一些和底層系統有關的操作,例如TCP連線返回的錯誤等。這些錯誤發生時,會被Node 推遲到下一個迴圈中執行。

輪詢階段

這個階段是用來執行和 IO 操作有關的回撥的,Node會向作業系統詢問是否有新的 IO 事件已經觸發,然後會執行響應的事件回撥。幾乎所有除了 定時器事件、 setImmediate()close callbacks 之外操作都會在這個階段執行。

check 階段

這個階段會執行 setImmediate() 設定的任務。

close callbacks 階段

如果一個 sockethandle(控制程式碼) 突然被關閉了,例如通過 socket.destroy() 關閉了,close 事件將會在這個階段發出。

事件迴圈的具體執行

事件迴圈初始化之後,會按照上圖所示的流程進行:

  1. 首先會依次執行 定時器中的任務、 pending callback 回撥;
  2. 然後進入到 idleprepare 階段,這裡會執行 Node 內部的一些邏輯;
  3. 然後進入到 poll 輪詢階段。在這個階段會執行所有的 IO 回撥,如 讀取檔案,網路操作等。 poll 階段有一個 poll queue 任務佇列。這個階段的執行過程相對較長,具體如下:
  • 進入到本階段,會先檢查 timeout 定時佇列是否有可執行的任務,如果有,會跳轉到 定時器階段 執行。
  • 如果沒有 定時器任務 ,就會檢查 poll queue 任務佇列,如果不為空,會遍歷執行所有任務直到都執行完畢或者達到能執行的最大的任務數量。
  • poll queue 任務佇列執行完成後,會檢查 setImmediate 任務佇列是否有任務,如果有的話,事件迴圈會轉移到下一個 check 階段。
  • 如果沒有 setImmediate 任務,那麼,Node 將會在此等待,等待新的 IO 回撥的到來,並立刻執行他們。 注意 :這個等待不會一直等待下去,而是達到一個限定條件之後,繼續轉到下一個階段去執行。

setTimeout()setImmediate()

一個小祕密

其實也不算祕密,只是我是在剛剛查閱資料才知道的。 那就是:在 Node 中,setTimeout(callback, 0) 會被轉換為 setTimeout(callback, 1) 。 詳情請參考 這裡

setTimeout()setImmediate() 的執行順序

下面這兩個定時任務執行的順序在不同情況下,表現不一致。

setTimeout(function() {
    console.log('timeout');
}, 0);

setImmediate(function() {
    console.log('immediate');
});
複製程式碼

普通程式碼中設定定時器

如果在普通的程式碼執行階段(例如在最外層程式碼塊中),設定這兩個定時任務,他們的執行順序是不固定的。

  1. 首先,我們設定的 setTimeout(callback, 0) 已經被轉換成為 setTimeout(callback, 1) ,所以進入 定時器 階段時,會根據當前時間判斷定時是否超過了 1ms
  2. 事件迴圈在進入定時器階段之前會由系統呼叫方法來更新當前時間,由於系統中同時執行著其他的程式,系統需要等待其他程式的程式執行結束才能獲取準確時間,所以更新得到的時間可能會有一定的延遲。
  3. 更新時間時,若沒有延遲,定時不到 1ms ,immediate 任務會先執行;如果存在延遲,並且這個時間達到了 1ms 的界限, timeout 任務就會首先執行。

在IO回撥中設定定時器

如果我們在 IO 回撥中設定了這兩個定時器,那麼 setImmediate 任務會首先執行,原因如下:

  1. 進入 poll phase 輪詢階段之前會先檢查是否有 timer 定時任務。
  2. 如果沒有 timer 定時任務,才會執行後面的 IO 回撥。
  3. 我們在 IO 回撥中設定 setTimeout 定時任務,這時已經過了 timer 檢查階段,所以 timer 定時任務會被推遲到下一個迴圈中執行。

process.nextTick()

無論在事件迴圈的哪個階段,只要使用 process.nextTick() 新增了回撥任務,Node 都會在進入下一階段之前把 nextTickQueue 佇列中的任務執行完。

setTimeout(function() {
    setImmediate(() => {
        console.log('immediate');
    });
    process.nextTick(() => {
        console.log('nextTick');
    });
}, 0);
// nextTick
// immediate
複製程式碼

上述程式碼中,總是先執行 nextTick 任務,就是因為在迴圈在進入下一個階段之前會先執行 nextTickQueue 中的任務。下面程式碼的執行結果也符合預期。

setImmediate(() => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    process.nextTick(() => {
        console.log('nextTick');
    });
});
// nextTick
// timeout
複製程式碼

原文在這裡

相關文章