事件循環,即 Event Loop
,其實就是 JS 管理事件執行的一個流程,具體的管理方法由 JS 運行的環境決定,目前 JS 的主要運行環境有瀏覽器和 Node。javascript
瀏覽器和 Node 的事件循環,都是先初始化一個循環,執行同步代碼,遇到異步操做時,會將其交給對應的線程處理,主線程則繼續往下執行,異步操做執行完畢後,對應的 callback 回調會被推入事件隊列,並在合適的時機執行。每執行一次循環體的過程,咱們稱之爲一個 Tick。html
與瀏覽器不一樣的是,Node 的循環分爲幾個階段,每一個階段分別處理不一樣的事件,而瀏覽器的循環不存在這樣的階段劃分。下面咱們介紹一下 Node 事件循環的幾個階段。java
Node 的事件循環分爲幾個階段,以下圖(除了 incoming,每個方框表明一個階段):node
咱們先簡單看看每一個階段的做用,也就是在一個 Tick 中,Node 是按照怎樣的順序工做的:npm
setTimeout
、setInterval
的回調;setImmediate
的回調;socket.on('close', ...)
每一個階段都對應一個 FIFO 的隊列,循環進入某個階段後,只有在兩種狀況下會跳出該階段進入下一個階段,一是將其隊列中的回調所有執行完,二是執行數達到了該階段的上限。api
接下來咱們重點看一下 poll 階段,poll 階段主要作兩件事情:瀏覽器
maxPollTime
;如下爲 poll 階段的流程:markdown
setImmediate
回調;maxPollTime
)。注意:poll 階段空閒,即 poll 隊列爲空的時候,一旦有新的 setImmediate
回調,循環就會結束 poll 進入 check 階段;或者一旦有新的 timer 計時結束,循環就會繞回 Timers 階段;若是同時出現二者的回調,setImmediate
的優先級更高併發
首先明確 setImmediate
和 setTimeout
各自的執行時機:setImmediate
是一個特殊的定時器,它的回調會在 check 階段執行,也就是緊跟在 poll 階段後面執行;setTimeout
的回調會在達到 delay 時間後,儘快執行,前提是計時結束並把回調推入了 Timers 的隊列。 另外還需明確一點,事件循環除了可以正常進入 Timers 階段外,poll 階段一旦空閒而且沒有待執行的 setImmediate
回調,就會去檢查是否有計時結束的 timer,若是有的話,就會繞回到 Timers 階段,因此 setTimeout
回調的執行時機實際上有兩個。less
好了,咱們如今來看 setImmediate
和 setTimeout(fn, 0)
回調的執行順序,能夠分爲如下兩種狀況:
setImmediate
的回調會先於 setTimeout(fn, 0) 執行如下示例中,咱們都假定 setImmediate
的回調爲 cb_immediate
,setTimeout(fn, 0)
的回調爲 cb_timeout
。
代碼以下:
// test_timer.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製代碼
咱們屢次運行會發現,「immediate」始終會在「timeout」以前打印:
$ node test_timer.js
immediate
timeout
複製代碼
下面咱們分析一下 cb_immediate
始終先執行的緣由。 首先,I/O 操做的回調在 poll 隊列裏,是在 poll 階段執行的。前面咱們提到,poll 階段一旦空閒,就會檢查是否有待執行的 setImmediate
回調,若是有,就會結束等待進入 check 階段,因此即便這時有新的 timer 計時結束,也要等到 check、close 都結束並進入新的 Tick 才能執行。poll 檢查發現 cb_immediate
待執行,因此循環直接進入 check 階段執行 cb_immediate
,而 cb_timeout
最快也要在下一個 Tick 纔會被執行。
代碼以下:
setTimeout(() => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {1
console.log('immediate');
});
}, 0);
複製代碼
屢次運行後咱們會發現,「immediate」始終比「timeout」先打印。 外層 setTimeout
的回調是在 Timers 階段執行的,執行完回調後,事件循環繼續日後面走,走到 poll 階段後,一旦 poll 空閒,就會檢查並發現有待執行的 setImmediate
回調,發現存在 cb_immediate
,因而循環直接進入 check 階段,率先執行 cb_immediate
。
綜合上面兩個示例,咱們能夠看出,在同一個異步操做的回調中同時調用 setImmediate
和 setTimeout(fn, 0)
,setImmediate
之因此始終會先執行,正是因爲計時器一旦錯過了 Timers 階段,下一個執行回調的時機在 poll 階段,而 poll 階段檢查過程當中,setImmediate
的優先級高於 setTimeout
,一旦發現有待執行的 setImmediate
回調,循環就會繼續往下走,因此 setImmediate
回調的執行永遠先於 setTimeout
回調。
咱們接着看一下在主模塊同時調用 setImmediate
和 setTimeout(fn, 0)
的狀況。
代碼以下:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {1
console.log('immediate');
});
複製代碼
運行以上代碼,咱們會發現,時而先打印出「timeout」,時而先打印出「immediate」,即二者的執行順序是不肯定的。
咱們先理一下主要流程:
setTimeout
交給定時器線程處理;setImmediate
,即調用 libuv 提供的一個 API,該 API 會將 cb_immediate
放入 check 階段的隊列;cb_timeout
),若爲空,繼續往下執行;setImmediate
回調(即 cb_immediate
),進入 check 階段;cb_Immediate
根據以上流程能夠看出,cb_immediate
和 cb_timeout
的執行順序,就取決於第 e 步中 Timers 的隊列是否已經存在 cb_timeout
,若是存在,則 cb_timeout
先執行,不然 cb_immediate
先執行。
這裏補充一點,在 Node 中,setTimeout(fn, 0)
會被強制改成 setTimeout(fn, 1)
,這一點在官方文檔中有相關說明:
When delay is larger than 2147483647 or less than 1, the delay will be set to 1.
若是循環進入 Timers 階段的時候,距離 setTimeout
執行已通過去了 1ms,而且 cb_timeout
已經被推入 Timers 的隊列,那麼循環就會取出 cb_timeout
並執行;反之,若是循環進入 Timers 階段的時候,cb_timeout
尚未在隊列內,那麼 cb_timeout
就不會在這個 Tick 被執行,cb_immediate
會先執行。
而循環進入 Timers 階段的時候,是否已經通過了 1ms,會受到多方面的影響,包括同步代碼執行所花費的時間,以及系統的性能,機器的狀態差別也會致使每次運行的結果不一樣。因此同時在主模塊調用 setImmediate
和 setTimeout(fn, 0)
,兩個回調的執行順序是不肯定的。
你們可能會發現,咱們在介紹事件循環的階段時,process.nextTick()
沒有出如今任何一個階段。這是由於 process.nextTick()
不屬於任何一個階段,事實上,它是在 「階段之間」 執行的。 在任何一個階段調用了 process.nextTick()
,nextTick 的回調都會在當前階段結束後當即執行,全部回調執行完後,事件循環才進入下一個階段,因此過多的或長時間執行的 nextTick 回調,實際上是能夠阻塞整個事件循環的,因此得當心使用 nextTick。
咱們回顧一下二者的回調的執行時機,process.nextTick()
的回調是在當前階段結束後就當即執行,setImmediate()
的回調是在每一個循環的 check 階段執行。細心的讀者就會發現,process.nextTick()
彷佛比 setImmediate()
更「immediate」,更即時。 事實上官方也提到了這一點,二者的名字應該倒過來才比較合理,但修更名字的話,影響範圍太大了,npm 上全部用到這兩個方法的包都會受到影響,因此即便二者的名字存在必定的迷惑性,可是目前看來,更名是不現實也是不可能的。
二者有如下兩點區別:
和瀏覽器同樣,Node 也會維護一個微任務列表。和瀏覽器不一樣的是,瀏覽器是在宏任務即將結束的時候,檢查宏任務對應的微任務列表是否爲空,若不爲空,則執行全部微任務後,再進入下一個 Tick。而 Node 的微任務執行時機是在各個階段之間,一個階段結束後,事件循環會去檢查微任務列表是否爲空,若不爲空,則執行完全部微任務後,才進入循環的下一個階段。 注意,setImmediate
是 Node 特有的宏任務,process.nextTick
是 Node 特有的微任務。