面試之Event Loop,nextTick()和setImmediate()區別分析
從Vue.js源碼看nextTick機制
從Vue.js源碼看nextTick機制
詳解JavaScript中的Event Loop(事件循環)機制
JavaScript中的執行機制 - 掘金
序
一直以來,我對Event Loop的認知界定都是可知可不知的分級,因此僅僅保留淺顯的概念,從未真正學習過,直到看了這篇文章——《這一次,徹底弄懂 JavaScript 執行機制》。該文作者寫的非常友好,從最小的例子展開,讓我獲益匪淺,但最後的示例牽扯出了chrome和Node下的運行結果迥異,我很好奇,我覺得有必要對這一塊知識進行學習。
由於上述原因,本文誕生,原本我計劃全文共分3部分來展開:規範、實現、應用。但遺憾的是由於自己的認知尚淺,在如何根據Event Loop的特性來設想應用場景時,實在沒有什麼產出,導致有關應用的篇幅過小,故不在標題中作體現了。
(本文所有代碼運行環境僅包含Node v8.9.4以及 Chrome v63)
PART 1:規範
為什麼要有Event Loop?
因為Javascript設計之初就是一門單線程語言,因此為了實現主線程的不阻塞,Event Loop這樣的方案應運而生。
小測試(1)
先來看一段代碼,列印結果會是?
console.log(1)nnsetTimeout(() => {n console.log(2)n}, 0)nnPromise.resolve().then(() => {ntconsole.log(3)n}).then(() => {ntconsole.log(4)n})nnconsole.log(5)n

不熟悉Event Loop的我嘗試進行如下分析:
- 首先,我們先排除非同步代碼,先把同步執行的代碼找出,可以知道先列印的一定是
1、5 - 但是,setTimeout和Promise是否有優先順序?還是看執行順序?
- 還有,Promise的多級then之間是否會插入setTimeout?
帶著困惑,我試著運行了一下代碼,正確結果是:1、5、3、4、2。
那這到底是為什麼呢?
定義
看來需要先從規範定義入手,於是查閱一下HTML規範,規範著實詳(luo)細(suo),我就不貼了,提煉下來關鍵步驟如下:
- 執行最舊的task(一次)
- 檢查是否存在microtask,然後不停執行,直到清空隊列(多次)
- 執行render
好傢夥,問題還沒搞明白,一下子又多出來2個概念task和microtask,讓懵逼的我更加凌亂了。。。
不慌不慌,通過仔細閱讀文檔得知,這兩個概念屬於對非同步任務的分類,不同的API註冊的非同步任務會依次進入自身對應的隊列中,然後等待Event Loop將它們依次壓入執行棧中執行。
task主要包含:
microtask主要包含:setTimeout、setInterval、setImmediate、I/O、UI交互事件Promise、process.nextTick、MutaionObserver
整個最基本的Event Loop如圖所示:
- queue可以看做一種數據結構,用以存儲需要執行的函數
- timer類型的API(setTimeout/setInterval)註冊的函數,等到期後進入task隊列(這裡不詳細展開timer的運行機制)
- 其餘API註冊函數直接進入自身對應的task/microtask隊列
- Event Loop執行一次,從task隊列中拉出一個task執行
- Event Loop繼續檢查microtask隊列是否為空,依次執行直至清空隊列

繼續測試(2)
這時候,回頭再看下之前的測試(1),發現概念非常清晰,一下子就得出了正確答案,感覺自己萌萌噠,再也不怕Event Loop了~
接著,準備挑戰一下更高難度的問題(本題出自序中提到的那篇文章,我先去除了process.nextTick:
console.log(1)nnsetTimeout(() => {n console.log(2)n new Promise(resolve => {n console.log(4)n resolve()n }).then(() => {n console.log(5)n })n})nnnew Promise(resolve => {n console.log(7)n resolve()n}).then(() => {n console.log(8)n})nnsetTimeout(() => {n console.log(9)n new Promise(resolve => {n console.log(11)n resolve()n }).then(() => {n console.log(12)n })n})n
分析如下:
- 同步運行的代碼首先輸出:
1、7 - 接著,清空microtask隊列:
8 - 第一個task執行:
2、4 - 接著,清空microtask隊列:
5 - 第二個task執行:
9、11 - 接著,清空microtask隊列:
12
在chrome下運行一下,全對!
自信的我膨脹了,準備加上process.nextTick後在node上繼續測試。我先測試第一個task,代碼如下:
console.log(1)nnsetTimeout(() => {n console.log(2)n new Promise(resolve => {n console.log(4)n resolve()n }).then(() => {n console.log(5)n })n process.nextTick(() => {n console.log(3)n })n})nnnew Promise(resolve => {n console.log(7)n resolve()n}).then(() => {n console.log(8)n})nnprocess.nextTick(() => {n console.log(6)n})n
有了之前的積累,我這回自信的寫下了答案:1、7、8、6、2、4、5、3。
然而,帥不過3秒,正確答案是:1、7、6、8、2、4、3、5。
我陷入了困惑,不過很快明白了,這說明**process.nextTick註冊的函數優先順序高於Promise**,這樣就全說的通了~
接著,我再測試第二個task:
console.log(1)nnsetTimeout(() => {n console.log(2)n new Promise(resolve => {n console.log(4)n resolve()n }).then(() => {n console.log(5)n })n process.nextTick(() => {n console.log(3)n })n})nnnew Promise(resolve => {n console.log(7)n resolve()n}).then(() => {n console.log(8)n})nnprocess.nextTick(() => {n console.log(6)n})nnsetTimeout(() => {n console.log(9)n process.nextTick(() => {n console.log(10)n })n new Promise(resolve => {n console.log(11)n resolve()n }).then(() => {n console.log(12)n })n})n
吃一塹長一智,這次我掌握了microtask的優先順序,所以答案應該是:
- 第一個task輸出:
1、7、6、8、2、4、3、5 - 然後,第二個task輸出:
9、11、10、12
然而,啪啪打臉。。。
我第一次執行,輸出結果是:1、7、6、8、2、4、9、11、3、10、5、12(即兩次task的執行混合在一起了)。我繼續執行,有時候又會輸出我預期的答案。
現實真的是如此莫名啊!啊!啊!
(啊,不好意思,血一時止不住)所以,這到底是為什麼???
PART 2:實現
俗話說得好:
規範是人定的,代碼是人寫的。 ——無名氏
規範無法囊括所有場景,雖然
chrome和node都基於v8引擎,但引擎只負責管理內存堆棧,API還是由各runtime自行設計並實現的。
小測試(3)
Timer是整個Event Loop中非常重要的一環,我們先從timer切入,來切身體會下規範和實現的差異。
首先再來一個小測試,它的輸出會是什麼呢?

setTimeout(() => {ntconsole.log(2)n}, 2)nnsetTimeout(() => {ntconsole.log(1)n}, 1)nnsetTimeout(() => {ntconsole.log(0)n}, 0)n
沒有深入接觸過timer的同學如果直接從代碼中的延時設置來看,會回答:0、1、2。
而另一些有一定經驗的同學可能會回答:2、1、0。因為MDN的setTimeout文檔中提到HTML規範最低延時為4ms:
(補充說明:最低延時的設置是為了給CPU留下休息時間)
In fact, 4ms is specified by the HTML5 spec and is consistent across browsers released in 2010 and onward. Prior to (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.
而真正痛過的同學會告訴你,答案是:
1、0、2。並且,無論是chrome還是node下的運行結果都是一致的。
Chrome中的timer
從測試(3)結果可以看出,0ms和1ms的延時效果是一致的,那背後的原因是為什麼呢?我們先查查blink的實現。
(Blink代碼託管的地方我都不知道如何進行搜索,還好文件名比較明顯,沒花太久,找到了答案)
(我直接貼出最底層代碼,上層代碼如有興趣請自行查閱)
// https://chromium.googlesource.com/chromium/blink/+/master/Source/core/frame/DOMTimer.cpp#93nndouble intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond); n
這裡interval就是傳入的數值,可以看出傳入0和傳入1結果都是oneMillisecond,即1ms。
這樣解釋了為何1ms和0ms行為是一致的,那4ms到底是怎麼回事?我再次確認了HTML規範,發現雖然有4ms的限制,但是是存在條件的,詳見規範第11點:
If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
並且有意思的是,MDN英文文檔的說明也已經貼合了這個規範。
我斗膽推測,一開始HTML5規範確實有定最低4ms的規範,不過在後續修訂中進行了修改,我認為甚至不排除規範在向實現看齊,即逆向影響。
Node中的timer
那node中,為什麼0ms和1ms的延時效果一致呢?
(還是github託管代碼看起來方便,直接搜到目標代碼)
// https://github.com/nodejs/node/blob/v8.9.4/lib/timers.js#L456nnif (!(after >= 1 && after <= TIMEOUT_MAX))n after = 1; // schedule on next tick, follows browser behaviorn
代碼中的注釋直接說明了,設置最低1ms的行為是為了向瀏覽器行為看齊。
Node中的Event Loop
上文的timer算一個小插曲,我們現在回歸本文核心——Event Loop。
讓我們聚焦在node的實現上,blink的實現本文不做展開,主要是因為:
chrome行為目前看來和規範一致- 可參考的文檔不多
- 不會搜索,根本不知道核心代碼從何找起。。。
(略過所有研究過程。。。)
直接看結論,下圖是node的Event Loop實現:

補充說明:
Node的Event Loop分階段,階段有先後,依次是- expired timers and intervals,即到期的setTimeout/setInterval
- I/O events,包含文件,網路等等
- immediates,通過setImmediate註冊的函數
- close handlers,close事件的回調,比如TCP連接斷開
- 同步任務及每個階段之後都會清空microtask隊列
- 優先清空next tick queue,即通過
process.nextTick註冊的函數 - 再清空other queue,常見的如Promise
- 而和規範的區別,在於node會清空當前所處階段的隊列,即執行所有task
重新挑戰測試(2)
了解了實現,再回頭看測試(2):
// 代碼簡略表示n// 1nsetTimeout(() => {nt// ...n})nn// 2nsetTimeout(() => {nt// ...n})n
可以看出由於兩個setTimeout延時相同,被合併入了同一個expired timers queue,而一起執行了。所以,只要將第二個setTimeout的延時改成超過2ms(1ms無效,詳見上文),就可以保證這兩個setTimeout不會同時過期,也能夠保證輸出結果的一致性。
那如果我把其中一個setTimeout改為setImmediate,是否也可以做到保證輸出順序?
答案是不能。雖然可以保證setTimeout和setImmediate的回調不會混在一起執行,但無法保證的是setTimeout和setImmediate的回調的執行順序。
在node下,看一個最簡單的例子,下面代碼的輸出結果是無法保證的:
setTimeout(() => {ntconsole.log(0)tn})nnsetImmediate(() => {ntconsole.log(1)n})nn// ornsetImmediate(() => {ntconsole.log(0)n})nnsetTimeout(() => {ntconsole.log(1)tn})n
問題的關鍵在於setTimeout何時到期,只有到期的setTimeout才能保證在setImmediate之前執行。
不過如果是這樣的例子(2),雖然基本能保證輸出的一致性,不過強烈不推薦:
// 先使用setTimeout註冊nsetTimeout(() => {nt// ...n})nn// 一系列micro tasks執行,保證setTimeout順利到期nnew Promise(resolve => {nt// ...n})nprocess.nextTick(() => {nt// ...n})nn// 再使用setImmediate註冊,「幾乎」確保後執行nsetImmediate(() => {nt// ...n})n
或者換種思路來保證順序:
const fs = require(fs)nnfs.readFile(/path/to/file, () => {n setTimeout(() => {n console.log(timeout)n })n setImmediate(() => {n console.log(immediate)n })n})n
那,為何這樣的代碼能保證setImmediate的回調優先於setTimeout的回調執行呢?
因為當兩個回調同時註冊成功後,當前node的Event Loop正處於I/O queue階段,而下一個階段是immediates queue,所以能夠保證即使setTimeout已經到期,也會在setImmediate的回調之後執行。
PART 3:應用
由於也是剛剛學習Event Loop,無論是依託於規範還是實現,我能想到的應用場景還比較少。那掌握Event Loop,我們能用在哪些地方呢?
查Bug
正常情況下,我們不會碰到非常複雜的隊列場景。不過萬一碰到了,比如執行順序無法保證的情況時,我們可以快速定位到問題。
面試
那什麼時候會有複雜的隊列場景呢?比如面試,保不準會有這種稀奇古怪的測試,這樣就能輕鬆應付了~
執行優先順序
說回正經的,如果從規範來看,microtask優先於task執行。那如果有需要優先執行的邏輯,放入microtask隊列會比task更早的被執行,這個特性可以被用於在框架中設計任務調度機制。
如果從node的實現來看,如果時機合適,microtask的執行甚至可以阻塞I/O,是一把雙刃劍。
綜上,高優先順序的代碼可以用Promise/process.nextTick註冊執行。
執行效率
從node的實現來看,setTimeout這種timer類型的API,需要創建定時器對象和迭代等操作,任務的處理需要操作小根堆,時間複雜度為O(log(n))。而相對的,process.nextTick和setImmediate時間複雜度為O(1),效率更高。
如果對執行效率有要求,優先使用process.nextTick和setImmediate。
1.node中使用定時器的問題在於,它並非精確的.譬如setTimeout()設定一個任務在10ms後執行,但是在9ms後,有一個任務佔用了5ms,再次輪到定時器時,已經耽誤了4ms.
好了node中的定時器就簡單的講這麼多.
2.看代碼:
process.nextTick(function(){n console.log("延遲執行");n});nconsole.log("正常執行1");nconsole.log("正常執行2");nconsole.log("正常執行3");nconsole.log("正常執行4");n
通過這個例子,我想大家很清楚的就能看到nextTick()是用來幹嘛的了.主要是用來非同步執行的.
在看代碼:
setImmediate(function(){n console.log("延遲執行");n});nconsole.log("正常執行");n
我們發現setImmediate也是非同步執行的.奇怪了
那麼它與nextTick()有什麼區別呢?
看代碼:
代碼一:
process.nextTick(function(){n console.log("nextTick延遲")n});nsetImmediate(function(){n console.log("setImmediate延遲");n});nconsole.log("正常執行");n
結果:
代碼二:
setImmediate(function(){n console.log("setImmediate延遲");n});nprocess.nextTick(function(){n console.log("nextTick延遲")n});nconsole.log("正常執行");n
結果:

發現代碼雖然順序不一樣,但是執行的結果是一樣的.
從結果可以發現:
- nextTick()的回調函數執行的優先順序要高於setImmediate();
- process.nextTick()屬於idle觀察者,setImmediate()屬於check觀察者.在每一輪循環檢查中,idle觀察者先於I/O觀察者,I/O觀察者先於check觀察者.
- 在具體實現上,process.nextTick()的回調函數保存在一個數組中,setImmediate()的結果則是保存在鏈表中.在行為上,process.nextTick()在每輪循環中會將數組中的回調函數全部執行完.而setImmediate()在每輪循環中執行鏈表中的一個回調函數.
//加入2個nextTick()的回調函數nprocess.nextTick(function(){n console.log("nextTick延遲執行1");n});nprocess.nextTick(function(){n console.log("nextTick延遲執行2");n});n//加入兩個setImmediate()回調函數nsetImmediate(function(){n console.log("setImmediate延遲執行1");n process.nextTick(function(){n console.log("強勢插入");n });n});nsetImmediate(function(){n console.log("setImmediate延遲執行2");n});nconsole.log("正常執行");n
從執行結果上看出:當第一個setImmediate()的回調函數執行完後,並沒有立即執行第二個,而是進入了下一輪循環,再次按nextTick()優先,setImmediate()次後的順序執行.之所以這樣設計,是為了保證每次循環能夠較快的執行結束.防止CPU佔用過多而阻塞後續I/O調用的情況.
區別:
1.在理解兩者的區別之前要說一下輪詢
前面博客也有記錄,nodejs中是事件驅動的,有一個循環線程一直從事件隊列中取任務執行或者I/O的操作轉給後台線程池來操作,把這個循環線程的每次執行的過程算是一次輪詢.
2.setImmediate()的使用
即時計時器立即執行工作,它是在事件輪詢之後執行,為了防止輪詢阻塞,每次只會調用一個。
3.Process.nextTick()的使用
它和setImmediate()執行的順序不一樣,它是在事件輪詢之前執行,為了防止I/O飢餓,所以有一個默認process.maxTickDepth=1000來限制事件隊列的每次循環可執行的nextTick()事件的數目。
4.總結
在網上百度的關於它們的總結:
nextTick()的回調函數執行的優先順序要高於setImmediate();
process.nextTick()屬於idle觀察者,setImmediate()屬於check觀察者.在每一輪循環檢查中,idle觀察者先於I/O觀察者,I/O觀察者先於check觀察者.
在具體實現上,process.nextTick()的回調函數保存在一個數組中,
setImmediate()的結果則是保存在鏈表中.在行為上,process.nextTick()在每輪循環中會將數組中的回調函數全部執行完.而setImmediate()在每輪循環中執行鏈表中的一個回調函數.//加入2個nextTick()的回調函數nprocess.nextTick(function(){n console.log("nextTick延遲執行A");n});nprocess.nextTick(function(){n console.log("nextTick延遲執行B");n setImmediate(function(){n console.log("setImmediate延遲執行C");n });n process.nextTick(function(){n console.log("nextTick延遲執行D");n });n});nnn//加入兩個setImmediate()回調函數nsetImmediate(function(){n console.log("setImmediate延遲執行E");n process.nextTick(function(){n console.log("強勢插入F");n });n setImmediate(function(){n console.log("setImmediate延遲執行G");n });n});nsetImmediate(function(){n console.log("setImmediate延遲執行H");n process.nextTick(function(){n console.log("強勢插入I");n });n process.nextTick(function(){n console.log("強勢插入J");n });n setImmediate(function(){n console.log("setImmediate延遲執行K");n });n});nconsole.log("正常執行L");n
正常執行LnnextTick延遲執行AnnextTick延遲執行BnnextTick延遲執行DnsetImmediate延遲執行EnsetImmediate延遲執行HnsetImmediate延遲執行Cn強勢插入Fn強勢插入In強勢插入JnsetImmediate延遲執行GnsetImmediate延遲執行Kn
Promise then中回調為什麼是非同步執行?
Promise 的機制就是 then 回調函數必須非同步執行。為什麼?因為這樣保障了代碼執行順序的一致性。
先看一個場景:
promise.then(function(){ n if (trueOrFalse) { n // 同步執行 n foo(); n } else { n // 非同步執行 (如:使用第三方庫)n setTimeout(function(){ n foo(); n }) n } n}); nnbar();n
- 如果 promise then 回調是同步執行的,請問 foo() 和 bar() 函數誰先執行?答案是,如果 trueOrFalse 為 true 則 foo() 先執行,bar() 後執行;否則 bar() 先執行,foo() 後執行。在大部分情況下,你沒法預料到 trueOrFalse 的值,這也就意味著,你不能確定這段代碼真正的執行順序,這可能會導致一些難以想到的 bug。
- 如果 promise then 回調是非同步執行的,請問 foo() 和 bar() 函數誰先執行?答案一目了然,bar() 先執行,foo() 後執行。
所以為了保證代碼執行順序的一致性, then 回調必須保證是非同步的。
https://www.talkingcoder.com/article/6420924177101619200
setTimeout(0)單線程和非同步隊列
setTimeout和setInterval是JS內置的兩個定時器,使用很簡單,但這兩個方法背後的原理卻不簡單。我們知道,JS是單線程語言,在瀏覽器中,當JS代碼被載入時,瀏覽器會為其分配一個主線程來執行任務(函數),主線程會形成一個全局執行環境,執行環境採用棧的方式將待執行任務按順序依次來執行。但在瀏覽器中有一些任務是非常耗時的,比如http請求、定時器、事件回調等,為了保證其他任務的執行效率不被影響,JS在執行環境中維護了一個非同步隊列(也叫工作線程),並將這些任務放入隊列中進行等待,這些任務的執行時機並不確定,只有當主線程的任務執行完成以後,才會去檢查非同步隊列中的任務是否需要開始執行。這就是為什麼setTimeout(fn,0) 始終要等到最後執行的原因。關於單線程和非同步隊列問題請參考:setTimeout(0)https://www.talkingcoder.com/article/6394348388350903833
常青:JavaScript中的JS引擎的執行機制:探究Event LoopJavaScript中的JS引擎的執行機制:探究Event Loop
常青:JavaScript中的JS引擎的執行機制:探究Event Loop一、JavaScript是單線程
javascript是一門單線程語言,在最新的HTML5中提出了Web-Worker,但javascript是單線程這一核心仍未改變。與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?
所以,為了避免複雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,將來也不會改變。、
二、任務隊列
2.1 同步任務與非同步任務
單線程就意味著,所有任務需要排隊。所有任務可以分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個非同步任務可以執行了,該任務才會進入主線程執行。非同步執行的運行機制如下:
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要非同步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重複上面的第三步。
2.2 js 引擎模型
從宏觀角度講, js 的執行是單線程的. 所有的非同步結果都是通過 「任務隊列(Task Queue)」 來調度被調度. 消息隊列中存放的是一個個的任務(Task). 規範中規定, Task 分為兩大類, 分別是 Macro Task 和 Micro Task, 並且每個 Macro Task 結束後, 都要清空所有的 Micro Task. 宏觀上講, Macrotask 會進入 Macro Task Queue, Microtask 會進入 Micro Task Queue。而 Micro Task 被分到了兩個隊列中. 『Micro Task Queue』 存放 Promise 等 microtask. 而 『Tick Task Queue』 專門用於存放 process.nextTick 的任務.現在先來看看規範怎麼做的分類.
- Macrotask 包括:
setImmediatesetTimeoutsetInterval
- Microtask 包括:
process.nextTickPromiseObject.observeMutaionObserver
所說的, 『每個 Macro Task 結束後, 都要清空所有的 Micro Task『. 引擎會遍歷 Macro Task Queue, 對於每個 Macrotask 執行完畢後都要遍歷執行 Tick Task Queue 的所有任務, 緊接著再遍歷 Micro Task Queue 的所有任務. (nextTick 會優於 Promise執行)
三、Event Loop
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為Event Loop(事件循環)。

三種任務隊列中的代碼執行流程圖如下:

來個例子檢驗一下吧
console.log(main1);nnprocess.nextTick(function() {n console.log(process.nextTick1);n});nnsetTimeout(function() {n console.log(setTimeout);n process.nextTick(function() {n console.log(process.nextTick2);n });n}, 0);nnnew Promise(function(resolve, reject) {n console.log(promise);n resolve();n}).then(function() {n console.log(promise then);n});nnconsole.log(main2);n
輸出結果為
main1npromisenmain2nprocess.nextTick1npromise thennsetTimeoutnprocess.nextTick2n
分析如下
- 開始執行代碼,輸出 main1,process.nextTick 放入tickTaskQueen,setTimeout放入 macroTaskQueen, new Promise 執行 輸出 promise,then 方法 放入 MicroTaskQueen , 接著 最後一行代碼 console.log 輸出 main2
- 當前的 宏任務執行完畢,開始清空微任務,先清空tickTaskQueen ,執行 console.log(process.nextTick1); 輸出process.nextTick1;再清空MicroTaskQueen執行 console.log(promise then); 輸出promise then;微任務全部清空。
- 開始下次 eventLoop; 執行 setTimeout; 第一行 console.log(setTimeout); 輸出setTimeout; process.nextTick 將任務放入了tickTaskQueen;當前宏任務執行完畢;開始清空MicroTaskQueen,清空tickTaskQueen ,執行 console.log(process.nextTick2);輸出process.nextTick2;
四、Node.js的Event Loop
Node.js也是單線程的Event Loop,但是它的運行機制不同於瀏覽器環境。

根據上圖,Node.js的運行機制如下。
(1)V8引擎解析JavaScript腳本。
(2)解析後的代碼,調用Node API。
(3)libuv庫負責Node API的執行。它將不同的任務分配給不同的線程,形成一個Event Loop(事件循環),以非同步的方式將任務的執行結果返回給V8引擎。
(4)V8引擎再將結果返回給用戶。
Node.js還提供了另外兩個與"任務隊列"有關的方法:process.nextTick和setImmediate。它們可以幫助我們加深對"任務隊列"的理解。
process.nextTick(function A() {n console.log(1);n process.nextTick(function B(){console.log(2);});n});nnsetTimeout(function timeout() {n console.log(TIMEOUT FIRED);n}, 0)n// 1n// 2n// TIMEOUT FIREDn
上面代碼中,由於process.nextTick方法指定的回調函數,總是在當前"執行棧"的尾部觸發,所以不僅函數A比setTimeout指定的回調函數timeout先執行,而且函數B也比timeout先執行。這說明,如果有多個process.nextTick語句(不管它們是否嵌套),將全部在當前"執行棧"執行。
setImmediate(function (){n setImmediate(function A() {n console.log(1);n setImmediate(function B(){console.log(2);});n });nn setTimeout(function timeout() {n console.log(TIMEOUT FIRED);n }, 0);n});n// 1n// TIMEOUT FIREDn// 2n
上面代碼中,setImmediate和setTimeout被封裝在一個setImmediate裡面,它的運行結果總是1--TIMEOUT FIRED--2,這時函數A一定在timeout前面觸發。至於2排在TIMEOUT FIRED的後面(即函數B在timeout後面觸發),是因為setImmediate總是將事件註冊到下一輪Event Loop,所以函數A和timeout是在同一輪Loop執行,而函數B在下一輪Loop執行。
我們由此得到了process.nextTick和setImmediate的一個重要區別:多個process.nextTick語句總是在當前"執行棧"一次執行完,多個setImmediate可能則需要多次loop才能執行完。
推薦閱讀:
※深入 Promise(三)——命名 Promise
※libuv漫談之線程
※Egg.js 中 GraphQL 小試牛刀
※Node.js 實現 Hot Reload
※Node應用內存泄漏分析方法論與實戰
TAG:Nodejs |






