本文地址 http://www.cnblogs.com/jasonxuli/p/6074231.html
>>> 文末有簡單總結
什麼是事件循環(Event Loop)
事件循環能讓 Node.js 執行非阻塞 I/O 操做 -- 儘管JavaScript事實上是單線程的 -- 經過在可能的狀況下把操做交給操做系統內核來實現。
因爲大多數現代系統內核是多線程的,內核能夠處理後臺執行的多個操做。當其中一個操做完成的時候,內核告訴 Node.js,相應的回調就被添加到輪詢隊列(poll queue)並最終獲得執行。本主題隨後會解釋更多相關細節。
事件循環
Node.js 開始的時候會初始化事件循環,處理目標腳本,腳本可能會進行異步API調用、定時任務或者process.nextTick(),而後開始進行事件循環。
下面的表格簡要描述了事件循環的操做順序。
┌───────────────────────┐
┌─> │ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<───┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└──────────────────────———————─┘
注:每一個方框表明事件循環中的一個階段。
每一個階段都有一個須要執行的回調函數的先入先出(FIFO)隊列。同時,每一個階段都是特殊的,基本上,當事件循環進行到某個階段時,會執行該階段特有的操做,而後執行該階段隊列中的回調,直到隊列空了或者達到了執行次數限制。這時候,事件循環會進入下一個階段,循環往復。
因爲這些操做可能產生更多的計劃任務操做,而且輪詢階段處理的新事件會被加入到內核的隊列,輪詢事件被處理的時候會有新的輪詢事件加入。因而,長時回調任務會致使輪詢階段的時間超過了定時器的閾值。 詳情見 定時器(timers)和輪詢(poll)部分。
注:Windows 和 Unix/Linux 的實現有輕微的矛盾之處,但並不影響剛纔的描述。 最重要的部分都有了。實際上有七八個階段,但咱們關注的 -- Node.js 實際使用的 -- 就是上面這些。
階段總覽 (Phases Overview)
- 計時器(timers):本階段執行setTimeout() 和 setInterval() 計劃的回調;
- I/O 回調: 執行幾乎所有發生異常的 close 回調, 由定時器和setImmediate()計劃的回調;
- 空閒,預備(idle,prepare):只內部使用;
- 輪詢(poll): 獲取新的 I/O 事件;nodejs這時會適當進行阻塞;
- 檢查(check): 調用 setImmediate() 的回調;
- close callbacks: 例如 socket.on('close', ... );
在事件循環運行之間,Node.js 檢查是否有正在等待的異步I/O 或者定時器,若是沒有就清除並結束。
階段細節
定時器(timers)
定時器的用途是讓指定的回調函數在某個閾值後會被執行,具體的執行時間並不必定是那個精確的閾值。定時器的回調會在制定的時間事後儘快獲得執行,然而,操做系統的計劃或者其餘回調的執行可能會延遲該回調的執行。
注:從技術上來看,輪詢階段控制了定時器的執行時機。
例如,你設定了在100ms後執行某個操做,而後腳本開始執行一個須要95ms的文件讀取操做:
var fs = require('fs');
function someAsyncOperation (callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', callback); } var timeoutScheduled = Date.now(); setTimeout(function () { var delay = Date.now() - timeoutScheduled; console.log(delay + "ms have passed since I was scheduled"); }, 100); // do someAsyncOperation which takes 95 ms to completesomeAsyncOperation(function () { var startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { ; // do nothing } });
當事件循環進入輪詢階段時,隊列是空的(fs.readFile()還沒完成),所以時間會繼續流逝知道最快的定時器須要執行。過了95ms後,fs.readFile() 讀完文件了,它的回調被添加到輪詢隊列,這個回調須要執行10ms。等到這個回調執行完,隊列中沒有回調了,這時事件循環看到了最近到時的定時器,而後回到定時器階段(timers phase)來執行以前的定時器回調。
在這個例子中,從定義定時器到回調執行中間過了105ms。
注:爲了防止輪詢階段持續時間太長,libuv 會根據操做系統的不一樣設置一個輪詢的上限。
I/O callbacks
這個階段執行一些諸如TCP錯誤之類的系統操做的回調。例如,若是一個TCP socket 在嘗試鏈接時收到了 ECONNREFUSED錯誤,某些 *nix 系統會等着報告這個錯誤。這個就會被排到本階段的隊列中。
輪詢(poll)
輪詢階段有兩個主要功能:
1,執行已經到時的定時器腳本,而後
2,處理輪詢隊列中的事件。
當事件循環進入到輪詢階段卻沒有發現定時器時:
- 若是輪詢隊列非空,事件循環會迭代回調隊列並同步執行回調,直到隊列空了或者達到了上限(前文說過的根據操做系統的不一樣而設定的上限)。
- 若是輪詢隊列是空的:
-
- 若是有setImmediate()定義了回調,那麼事件循環會終止輪詢階段並進入檢查階段去執行定時器回調;
- 若是沒有setImmediate(),事件回調會等待回調被加入隊列並當即執行。
一旦輪詢隊列空了,事件循環會查找已經到時的定時器。若是找到了,事件循環就回到定時器階段去執行回調。
檢查(check)
這個階段容許回調函數在輪詢階段完成後當即執行。若是輪詢階段空閒了,而且有回調已經被 setImmediate() 加入隊列,事件循環會進入檢查階段而不是在輪詢階段等待。
setImmediate() 是個特殊的定時器,在事件循環中一個單獨的階段運行。它使用libuv的API 來使得回調函數在輪詢階段完成後執行。
基本上,隨着代碼的執行,事件循環會最終進入到等待狀態的輪詢階段,多是等待一個鏈接、請求等。然而,若是有一個setImmediate() 設置了一個回調而且輪詢階段空閒了,那麼事件循環會進入到檢查階段而不是等待輪詢事件。
---- 這車軲轆話說來講去的
關閉事件的回調(close callbacks)
若是一個 socket 或句柄(handle)被忽然關閉(is closed abruptly),例如 socket.destroy(), 'close' 事件會被髮出到這個階段。不然這種事件會經過 process.nextTick() 被髮出。
setImmediate() vs setTimeout()
這兩個很類似,但調用時機會的不一樣會致使它們不一樣的表現。
- setImmediate() 被設計成一旦輪詢階段完成就執行回調函數;
- setTimeout() 規劃了在某個時間值事後執行回調函數;
這兩個執行的順序會由於它們被調用時的上下文而有所不一樣。若是都是在主模塊調用,那麼它們會受到進程性能的影響(運行在本機的其餘程序會影響它們)。
例如,若是咱們在非 I/O 循環中運行下面的腳本(即在主模塊中),他倆的順序是不固定的,由於會受到進程性能的影響:
// timeout_vs_immediate.jssetTimeout(function timeout () {
console.log('timeout');
},0); setImmediate(function immediate () { console.log('immediate'); });
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
可是若是把它們放進 I/O 循環中,setImmediate() 的回調老是先執行:
// timeout_vs_immediate.jsvar fs = require('fs')
fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) })
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
setImmediate() 比 setTimeout() 優點的地方是 setImmediate() 在 I/O 循環中老是先於任何定時器,無論已經定義了多少定時器。
process.nextTick()
理解 process.nextTick()
你可能已經注意到了 process.nextTick() 沒有在上面那個表格裏出現,雖然它確實是一個異步API。這是由於它技術上不屬於事件循環。然而,nextTickQueue 會在當前操做結束後被處理,不論是在事件循環的哪一個階段。
回頭看看以前那個表格,你在某個階段的任什麼時候候調用它,它的全部回調函數都會在事件循環繼續進行以前獲得處理。有時候這會致使比較糟糕的狀況,由於它容許你用遞歸調用的方式去「阻塞」 I/O,這會讓事件循環沒法進入到輪詢階段。
爲何要容許這樣
部分是由於 Node.js 的設計哲學:API 應該老是異步的,即便本不須要是異步。
blablabla,後面幾段看的我有點尷尬+暈。既尷尬又暈是以爲這幾段說的有點囉嗦,並且舉的例子不合適。例子要麼是同步的,不是異步的。要麼是例子裏的寫法徹底能夠避免,好比應該先添加 'connect' 事件監聽再進行 .connect() 操做;又或者變量聲明最好放在變量使用以前,能夠避免變量的提早聲明和當時賦值的麻煩。
難道是我沒理解裏面的祕辛?
process.nextTick() vs setTimeout()
這兩個函數有些類似可是名字讓人困惑:
- process.netxtTick() 在事件循環的當前階段當即生效;
- setImmediate() 生效是在接下來的迭代或者事件循環的下一次tick;
本質上,它們的名字應該互換一下。process.nextTick() 比 setImmediate() 更「馬上」執行,但這是個歷史問題無法改變。若是改了,npm上大堆的包就要掛了。
咱們推薦開發者在全部狀況下都使用 setImmediate() 由於它更顯而易見(reason about),另外兼容性也更廣,例如瀏覽器端。
爲何使用 process.nextTick()
有兩大緣由:
- 容許用戶處理錯誤,清理不須要的資源,或許在事件循環結束前再次嘗試發送請求;
- 必須讓回調函數在調用棧已經清除(unwound)後而且事件循環繼續下去以前執行;
下面的兩個例子都是相似的,即在 line1 派發事件,卻在 line2 才添加監聽,所以監聽的回調是不可能被執行到的。
因而能夠用 process.nextTick() 使得當前調用棧先執行完畢,也即先執行 line2 註冊事件監聽,而後在 nextTick 派發事件。
const EventEmitter = require('events');
const util = require('util'); function MyEmitter() { EventEmitter.call(this); // use nextTick to emit the event once a handler is assigned process.nextTick(function () { this.emit('event'); }.bind(this)); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', function() { console.log('an event occurred!'); });
翻譯總結:
這篇文章寫的不太簡練,也可能爲了有更多的受衆吧,我感受車軲轆話比較多,一個意思要說好幾遍。
從編程應用的角度簡單來講:
Node.js 中的事件循環大概有七八個階段,每一個階段都有本身的隊列(queue),須要等本階段的隊列處理完成後才進入其餘階段。階段之間會互相轉換,循環順序並非徹底固定的 ,由於不少階段是由外部的事件觸發的。
其中比較重要的是三個:
- 定時器階段 timers:
定時器階段執行定時器任務(setTimeOut(), setInterval())。
- 輪詢階段 poll:
輪詢階段由 I/O 事件觸發,例如 'connect','data' 等。這是比較重/重要的階段,由於大部分程序功能就是爲了 I/O 數據。
本階段會處理定時器任務和 poll 隊列中的任務,具體邏輯:
-
- 處理到期的定時器任務,而後
- 處理隊列任務,直到隊列空了或者達到上限
- 若是隊列任務沒了:
-
- 若是有 setImmediate(),終止輪詢階段並進入檢查階段去執行;
- 若是沒有 setImmediate(),那麼就查看有沒有到期的定時器,有的話就回到定時器階段執行回調函數;
- 檢查階段 check:
當輪詢階段空閒而且已經有 setImmediate() 的時候,會進入檢查階段並執行。
比較次要但也列在表格中的兩個:
- I/O 階段:
本階段處理 I/O 異常錯誤;
- 'close'事件回調:
本階段處理各類 'close' 事件回調;
關於 setTimeout(), setImmediate(), process.nextTick():
- setTimeout() 在某個時間值事後儘快執行回調函數;
- setImmediate() 一旦輪詢階段完成就執行回調函數;
- process.nextTick() 在當前調用棧結束後就當即處理,這時也必然是「事件循環繼續進行以前」 ;
優先級順序從高到低: process.nextTick() > setImmediate() > setTimeout()
注:這裏只是多數狀況下,即輪詢階段(I/O 回調中)。好比以前比較 setImmediate() 和 setTimeout() 的時候就區分了所處階段/上下文。
另:
關於調用棧,事件循環還能夠參考這篇文章:
這篇文章裏對事件任務區分了大任務(macro task) 、小任務(micro task),每一個事件循環只處理一個大任務 ,但會處理完全部小任務。
這一點和前面的文章說的不一樣。
examples of microtasks:html
process.nextTick
promises
Object.observe
examples of macrotasks:node
setTimeout
setInterval
setImmediate
I/O