事件迴圈中的各階段
Node.js 的事件迴圈流程大致如下:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
複製程式碼
每個階段都有自己的任務佇列,當本階段的任務佇列都執行完畢,或者達到了執行的最大任務數,就會進入到下一個階段。
timers 階段
這個階段會執行被 setTimeout
和 setInterval
設定的定時任務。
當然,這個定時並不是準確的,而是在超過了定時時間後,一旦得到執行機會,就立刻執行。
pending callbacks 階段
這個階段會執行一些和底層系統有關的操作,例如TCP連線返回的錯誤等。這些錯誤發生時,會被Node 推遲到下一個迴圈中執行。
輪詢階段
這個階段是用來執行和 IO 操作有關的回撥的,Node會向作業系統詢問是否有新的 IO 事件已經觸發,然後會執行響應的事件回撥。幾乎所有除了 定時器事件、 setImmediate()
和 close callbacks
之外操作都會在這個階段執行。
check 階段
這個階段會執行 setImmediate()
設定的任務。
close callbacks 階段
如果一個 socket
或 handle(控制程式碼)
突然被關閉了,例如通過 socket.destroy()
關閉了,close
事件將會在這個階段發出。
事件迴圈的具體執行
事件迴圈初始化之後,會按照上圖所示的流程進行:
- 首先會依次執行 定時器中的任務、
pending callback
回撥; - 然後進入到
idle
、prepare
階段,這裡會執行 Node 內部的一些邏輯; - 然後進入到
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');
});
複製程式碼
普通程式碼中設定定時器
如果在普通的程式碼執行階段(例如在最外層程式碼塊中),設定這兩個定時任務,他們的執行順序是不固定的。
- 首先,我們設定的
setTimeout(callback, 0)
已經被轉換成為setTimeout(callback, 1)
,所以進入定時器
階段時,會根據當前時間判斷定時是否超過了1ms
。 - 事件迴圈在進入定時器階段之前會由系統呼叫方法來更新當前時間,由於系統中同時執行著其他的程式,系統需要等待其他程式的程式執行結束才能獲取準確時間,所以更新得到的時間可能會有一定的延遲。
- 更新時間時,若沒有延遲,定時不到
1ms
,immediate
任務會先執行;如果存在延遲,並且這個時間達到了1ms
的界限,timeout
任務就會首先執行。
在IO回撥中設定定時器
如果我們在 IO 回撥中設定了這兩個定時器,那麼 setImmediate
任務會首先執行,原因如下:
- 進入
poll phase
輪詢階段之前會先檢查是否有timer
定時任務。 - 如果沒有
timer
定時任務,才會執行後面的 IO 回撥。 - 我們在 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
複製程式碼