Node中的事件循環

事件循環中的各階段

Node.js 的事件循環流程大體以下:javascript

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

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

timers 階段

這個階段會執行被 setTimeoutsetInterval 設置的定時任務。 固然,這個定時並非準確的,而是在超過了定時時間後,一旦獲得執行機會,就馬上執行。node

pending callbacks 階段

這個階段會執行一些和底層系統有關的操做,例如TCP鏈接返回的錯誤等。這些錯誤發生時,會被Node 推遲到下一個循環中執行。git

輪詢階段

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

check 階段

這個階段會執行 setImmediate() 設置的任務。網絡

close callbacks 階段

若是一個 sockethandle(句柄) 忽然被關閉了,例如經過 socket.destroy() 關閉了,close 事件將會在這個階段發出。socket

事件循環的具體執行

事件循環初始化以後,會按照上圖所示的流程進行:oop

  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) 。 詳情請參考 這裏ui

setTimeout()setImmediate() 的執行順序

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

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
複製代碼

原文在這裏

相關文章
相關標籤/搜索