接觸過事件循環的同窗大都會糾結一個點,就是在Node中setTimeout
和setImmediate
執行順序的隨機性。javascript
好比說下面這段代碼:java
setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); })
執行的結果是這樣子的:node
爲何會出現這種狀況呢?別急,咱們先往下看。git
咱們都知道,JavaScript是單線程的語言,對I/O
的控制是經過異步來實現的,具體是經過「事件循環」機制來實現。github
對於JavaScript中的單線程,指的是JavaScript執行在單線程中,而內部I/O
任務實際上是另有線程池來完成的。瀏覽器
在瀏覽器中,咱們討論事件循環,是以「從宏任務隊列中取一個任務執行,再取出微任務隊列中的全部任務」來分析執行代碼的。可是在Node環境中並不適用。具體的瀏覽器事件循環解析:傳送門異步
在Node中,事件循環的模型和瀏覽器相比大體相同,而最大的不一樣點在於Node中事件循環分不一樣的階段。具體咱們下面會討論到。本文核心也在這裏。socket
下面是事件循環不一樣階段的示意圖:ide
每一個階段都有一個先進先出的回調隊列要執行。而每一個階段都有本身的特殊之處。簡單來講,就是當事件循環進入某個階段後,會執行該階段特定的任意操做,而後纔會執行這個階段裏的回調。當隊列被執行完,或者執行的回調數量達到上限後,事件循環纔會進入下一個階段。函數
如下是各個階段詳情。
一個timer
指定一個下限時間而不是準確時間,在達到這個下限時間後執行回調。在指定的時間事後,timers
會盡早的執行回調,可是系統調度或者其餘回調的執行可能會延遲它們。
從技術上來講,poll
階段控制timers
何時執行,而執行的具體位置在timers
。
下限的時間有一個範圍:[1, 2147483647]
,若是設定的時間不在這個範圍,將被設置爲1。
這個階段執行一些系統操做的回調,好比說TCP鏈接發生錯誤。
系統內部的一些調用。
這是最複雜的一個階段。
poll
階段有兩個主要的功能:一是執行下限時間已經達到的timers
的回調,一是處理poll
隊列裏的事件。
注:Node不少API都是基於事件訂閱完成的,這些API的回調應該都在poll
階段完成。
如下是Node官網的介紹:
筆者把官網陳述的狀況以不一樣的條件分解,更加的清楚。(若是有誤,師請改正。)
當事件循環進入poll
階段:
poll
隊列不爲空的時候,事件循環確定是先遍歷隊列並同步執行回調,直到隊列清空或執行回調數達到系統上限。poll
隊列爲空的時候,這裏有兩種狀況。
setImmediate()
設定了回調,那麼事件循環直接結束poll
階段進入check
階段來執行check
隊列裏的回調。若是代碼沒有被設定setImmediate()
設定回調:
timers
,那麼此時事件循環會檢查timers
,若是有一個或多個timers
下限時間已經到達,那麼事件循環將繞回timers
階段,並執行timers
的有效回調隊列。timers
,這個時候事件循環是阻塞在poll
階段等待回調被加入poll
隊列。這個階段容許在poll
階段結束後當即執行回調。若是poll
階段空閒,而且有被setImmediate()
設定的回調,那麼事件循環直接跳到check
執行而不是阻塞在poll
階段等待回調被加入。
setImmediate()
其實是一個特殊的timer
,跑在事件循環中的一個獨立的階段。它使用libuv
的API
來設定在poll
階段結束後當即執行回調。
注:setImmediate()
具備最高優先級,只要poll
隊列爲空,代碼被setImmediate()
,不管是否有timers
達到下限時間,setImmediate()
的代碼都先執行。
若是一個socket
或handle
被忽然關掉(好比socket.destroy()
),close
事件將在這個階段被觸發,不然將經過process.nextTick()
觸發。
代碼重現,咱們會發現setTimeout
和setImmediate
在Node環境下執行是靠「隨緣法則」的。
好比說下面這段代碼:
setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); })
執行的結果是這樣子的:
爲何會這樣子呢?
這裏咱們要根據前面的那個事件循環不一樣階段的圖解來講明一下:
首先進入的是timers
階段,若是咱們的機器性能通常,那麼進入timers
階段,一毫秒已通過去了(setTimeout(fn, 0)
等價於setTimeout(fn, 1)
),那麼setTimeout
的回調會首先執行。
若是沒有到一毫秒,那麼在timers
階段的時候,下限時間沒到,setTimeout
回調不執行,事件循環來到了poll
階段,這個時候隊列爲空,此時有代碼被setImmediate()
,因而先執行了setImmediate()
的回調函數,以後在下一個事件循環再執行setTimemout
的回調函數。
而咱們在執行代碼的時候,進入timers
的時間延遲實際上是隨機的,並非肯定的,因此會出現兩個函數執行順序隨機的狀況。
那咱們再來看一段代碼:
var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
這裏咱們就會發現,setImmediate
永遠先於setTimeout
執行。
緣由以下:
fs.readFile
的回調是在poll
階段執行的,當其回調執行完畢以後,poll
隊列爲空,而setTimeout
入了timers
的隊列,此時有代碼被setImmediate()
,因而事件循環先進入check
階段執行回調,以後在下一個事件循環再在timers
階段中執行有效回調。
一樣的,這段代碼也是同樣的道理:
setTimeout(() => { setImmediate(() => { console.log('setImmediate'); }); setTimeout(() => { console.log('setTimeout'); }, 0); }, 0);
以上的代碼在timers
階段執行外部的setTimeout
回調後,內層的setTimeout
和setImmediate
入隊,以後事件循環繼續日後面的階段走,走到poll
階段的時候發現隊列爲空,此時有代碼被setImmedate()
,因此直接進入check
階段執行響應回調(注意這裏沒有去檢測timers
隊列中是否有成員到達下限事件,由於setImmediate()
優先)。以後在第二個事件循環的timers
階段中再去執行相應的回調。
綜上,咱們能夠總結:
setImmediate
的回調永遠先執行。對於這兩個,咱們能夠把它們理解成一個微任務。也就是說,它其實不屬於事件循環的一部分。
那麼他們是在何時執行呢?
無論在什麼地方調用,他們都會在其所處的事件循環最後,事件循環進入下一個循環的階段前執行。
舉個?:
setTimeout(() => { console.log('timeout0'); process.nextTick(() => { console.log('nextTick1'); process.nextTick(() => { console.log('nextTick2'); }); }); process.nextTick(() => { console.log('nextTick3'); }); console.log('sync'); setTimeout(() => { console.log('timeout2'); }, 0); }, 0);
結果是:
再解釋一下:
timers
階段執行外層setTimeout
的回調,遇到同步代碼先執行,也就有timeout0
、sync
的輸出。遇到process.nextTick
後入微任務隊列,依次nextTick1
、nextTick3
、nextTick2
入隊後出隊輸出。以後,在下一個事件循環的timers
階段,執行setTimeout
回調輸出timeout2
。
下面給出兩段代碼,若是可以理解其執行順序說明你已經理解透徹。
代碼1:
setImmediate(function(){ console.log("setImmediate"); setImmediate(function(){ console.log("嵌套setImmediate"); }); process.nextTick(function(){ console.log("nextTick"); }) }); // setImmediate // nextTick // 嵌套setImmediate
解析:事件循環check
階段執行回調函數輸出setImmediate
,以後輸出nextTick
。嵌套的setImmediate
在下一個事件循環的check
階段執行回調輸出嵌套的setImmediate
。
代碼2:
var fs = require('fs'); function someAsyncOperation (callback) { // 假設這個任務要消耗 95ms 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); // someAsyncOperation要消耗 95 ms 才能完成 someAsyncOperation(function () { var startCallback = Date.now(); // 消耗 10ms... while (Date.now() - startCallback < 10) { ; // do nothing } });
解析:事件循環進入poll
階段發現隊列爲空,而且沒有代碼被setImmediate()
。因而在poll
階段等待timers
下限時間到達。當等到95ms
時,fs.readFile
首先執行了,它的回調被添加進poll
隊列並同步執行,耗時10ms
。此時總共時間累積105ms
。等到poll
隊列爲空的時候,事件循環會查看最近到達的timer
的下限時間,發現已經到達,再回到timers
階段,執行timer
的回調。
若是有什麼問題,歡迎留言交流探討。
參考連接: