單線程編程會因阻塞I/O致使硬件資源得不到更優的使用。多線程編程也由於編程中的死鎖、狀態同步等問題讓開發人員頭痛。
Node在二者之間給出了它的解決方案:利用單線程,遠離多線程死鎖、狀態同步等問題;利用異步I/O,讓單線程遠離阻塞,以好使用CPU。html
實際上,node只是在應用層屬於單線程,底層其實經過libuv維護了一個阻塞I/O調用的線程池。前端
可是:在應用層面,JS是單線程的,業務代碼中不能存在耗時過長的代碼,不然可能會嚴重拖後續代碼(包括回調)的處理。若是遇到須要複雜的業務計算時,應當想辦法啓用獨立進程或交給其餘服務進行處理。node
在Node中,JS是在單線程中執行的沒錯,可是內部完成I/O工做的另有線程池,使用一個主進程和多個I/O線程來模擬異步I/O。
當主線程發起I/O調用時,I/O操做會被放在I/O線程來執行,主線程繼續執行下面的任務,在I/O線程完成操做後會帶着數據通知主線程發起回調。git
事件循環是Node的執行模型,正是這種模型使得回調函數很是廣泛。
在進程啓動時,Node便會建立一個相似while(true)的循環,執行每次循環的過程就是判斷有沒有待處理的事件,若是有,就取出事件及其相關的回調並執行他們,而後進入下一個循環。若是再也不有事件處理,就退出進程。github
Event loop是一種程序結構,是實現異步的一種機制。Event loop能夠簡單理解爲:編程
Node中事件循環階段解析:segmentfault
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
每一個階段都有一個FIFO的回調隊列(queue)要執行。而每一個階段有本身的特殊之處,簡單說,就是當event loop進入某個階段後,會執行該階段特定的(任意)操做,而後纔會執行這個階段的隊列裏的回調。當隊列被執行完,或者執行的回調數量達到上限後,event loop會進入下個階段。瀏覽器
Phases Overview 階段總覽微信
setTimeout()
、setInterval()
設定的回調。close callbacks
、setTimeout()
、setInterval()
、setImmediate()
的回調。setImmediate()
設定的回調。一個timer
指定一個下限時間而不是準確時間,定時器setTimeout()
和setInterval()
在達到這個下限時間後執行回調。在指定的時間事後,timers會盡早的執行回調,可是系統調度或者其餘回調的執行可能會延遲它們。
從技術上來講,poll
階段控制timers何時執行,而執行的具體位置在timers。
下限的時間有一個範圍:[1, 2147483647]
,若是設定的時間不在這個範圍,將被設置爲1。多線程
執行除了close callbacks
、setTimeout()
、setInterval()
、setImmediate()
回調以外幾乎全部回調,好比說TCP鏈接發生錯誤。
系統內部的一些調用。
這是最複雜的一個階段。poll
會檢索新的I/O events
,而且會在合適的時候阻塞,等待回調被加入。
poll階段有兩個主要的功能:一是執行下限時間已經達到的timers的回調,一是處理poll隊列裏的事件。
注:Node不少API都是基於事件訂閱完成的,這些API的回調應該都在poll階段完成。
當事件循環進入poll階段:
poll
隊列不爲空的時候,事件循環確定是先遍歷隊列並同步執行回調,直到隊列清空或執行回調數達到系統上限。poll
隊列爲空的時候,這裏有兩種狀況。
setImmediate()
設定了回調,那麼事件循環直接結束poll
階段進入check
階段來執行check
隊列裏的回調。若是代碼沒有被設定setImmediate()
設定回調:
Node的不少API都是基於事件訂閱完成的,好比fs.readFile,這些回調應該都在poll
階段完成。
setImmediate()
在這個階段執行。
這個階段容許在poll
階段結束後當即執行回調。若是poll
階段空閒,而且有被setImmediate()
設定的回調,那麼事件循環直接跳到check
執行而不是阻塞在poll階段等待poll 事件們 (poll events)被加入。
注意:若是進行到了poll
階段,setImmediate()具備最高優先級,只要poll
隊列爲空且註冊了setImmediate(),不管是否有timers
達到下限時間,setImmediate()的代碼都先執行。
若是一個socket或handle被忽然關掉(好比socket.destroy()
),close
事件將在這個階段被觸發,不然將經過process.nextTick()觸發。
對於Node中的異步I/O調用而言,回調函數不禁開發者來調用,從JS發起調用到I/O操做完成,存在一箇中間產物,叫請求對象。
在JS發起調用後,JS調用Node的核心模塊,核心模塊調用C++內建模塊,內建模塊經過libuv判斷平臺並進行系統調用。在進行系統調用時,從JS層傳入的方法和參數都被封裝在一個請求對象中,請求對象被放在線程池中等待執行。JS當即返回繼續後續操做。
在線程可用時,線程會取出請求對象來執行I/O操做,執行完後將結果放在請求對象中,並歸還線程。
在事件循環中,I/O觀察者會不斷的找到線程池中已經完成的請求對象,從中取出回調函數和數據並執行。
跑完當前執行環境下能跑完的代碼。每個事件消息都被運行直到完成爲止,在此以前,任何其餘事件都不會被處理。這和C等一些語言不通,它們可能在一個線程裏面,函數跑着跑着忽然停下來,而後其餘線程又跑起來了。JS這種機制的一個典型的壞處,就是當某個事件處理耗時過長時,後面的事件處理都會被延後,直到這個事件處理結束,在瀏覽器環境中運行時,可能會出現某個腳本運行時間過長,頁面無響應的提示。Node環境則可能出現大量用戶請求被掛起,不能及時響應的狀況。
Node中除了異步I/O以外,還有一些與I/O無關的異步API,分別是:setTimeout()
、setInterval()
、process.nextTick()
、setImmediate()
,他們並非像普通I/O操做那樣真的須要等待事件異步處理結束再進行回調,而是出於定時或延遲處理的緣由才設計的。
setTimeout()
與setInterval()
這兩個方法實現原理與異步I/O類似,只不過不用I/O線程池的參與。
使用它們建立的定時器會被放入timers
隊列的一個紅黑樹中,每次事件循環執行時會從相應隊列中取出並判斷是否超過定時時間,超過就造成一個事件,回調當即執行。
因此,和瀏覽器中同樣,這個並不精確,會被長時間的同步事件阻塞。
值得一提的是,在Node的setTimeout的源碼中:
// Node源碼 after *= 1; // coalesce to number or NaN if (!(after >= 1 && after <= TIMEOUT_MAX)) { if (after > TIMEOUT_MAX) { process.emitWarning(...); } after = 1; // schedule on next tick, follows browser behavior }
意思是若是沒有設置這個after,或者小於1,或者大於TIMEOUT_MAX(2^31-1),都會被強制設置爲1ms。也就是說setTimeout(xxx,0)其實等同於setTimeout(xxx,1)。
setImmediate()
setImmediate()是放在check
階段執行的,其實是一個特殊的timer,跑在event loop中一個獨立的階段。它使用libuv
的API來設定在 poll
階段結束後當即執行回調。
來看看這個例子:
setTimeout(function() { console.log('setTimeout') }, 0) setImmediate(function() { console.log('setImmediate') }) // 輸出不穩定
setTimeout與setImmediate前後入隊以後,首先進入的是timers
階段,若是咱們的機器性能通常或者加入了一個同步長耗時操做,那麼進入timers
階段,1ms已通過去了,那麼setTimeout的回調會首先執行。
若是沒有到1ms,那麼在timers
階段的時候,超時時間沒到,setTimeout回調不執行,事件循環來到了poll
階段,這個時候隊列爲空,此時有代碼被setImmediate(),因而先執行了setImmediate()的回調函數,以後在下一個事件循環再執行setTimemout的回調函數。
setTimeout(function() { console.log('set timeout') }, 0) setImmediate(function() { console.log('set Immediate') }) for (let i = 0; i < 100000; i++) {} // 能夠保證執行時間超過1ms // 穩定輸出: setTimeout setImmediate
這樣就能夠穩定輸出了。
再一個栗子:
const fs = require('fs') fs.readFile('./filePath.js', (err, data) => { setTimeout(() => console.log('setTimeout') , 0) setImmediate(() => console.log('setImmediate')) console.log('開始了') for (let i = 0; i < 100000; i++) {} }) // 輸出 開始了 setImmediate setTimeout
這裏咱們就會發現,setImmediate永遠先於setTimeout執行。
fs.readFile的回調是在poll
階段執行的,當其回調執行完畢以後,setTimeout與setImmediate前後入了timers
與check
的隊列,繼續到poll
,poll
隊列爲空,此時發現有setImmediate,因而事件循環先進入check
階段執行回調,以後在下一個事件循環再在timers
階段中執行setTimeout回調,雖然這個setTimeout已經到了超時時間。
再來個栗子:
一樣的,這段代碼也是同樣的道理:
setTimeout(() => { setImmediate(() => console.log('setImmediate') ); setTimeout(() => console.log('setTimeout') , 0); }, 0);
以上的代碼在timers
階段執行外部的setTimeout回調後,內層的setTimeout和setImmediate入隊,以後事件循環繼續日後面的階段走,走到poll
階段的時候發現隊列爲空,此時有代碼被setImmedate(),因此直接進入check
階段執行響應回調(注意這裏沒有去檢測timers
隊列中是否有成員到達超時事件,由於setImmediate()優先)。以後在下一個事件循環的timers
階段中再去執行相應的回調。
process.nextTick()
與Promise
對於這兩個,咱們能夠把它們理解成一個微任務。也就是說,它們其實不屬於事件循環的一部分。
有時咱們想要當即異步執行一個任務,可能會使用延時爲0的定時器,可是這樣開銷很大。咱們能夠換而使用process.nextTick()
,它會將傳入的回調放入nextTickQueue
隊列中,下一輪Tick以後取出執行,無論事件循環進行到什麼地步,都在當前執行棧的操做結束的時候調用,參見Nodejs官網。
process.nextTick方法指定的回調函數,老是在當前執行隊列的尾部觸發,多個process.nextTick語句老是一次執行完(無論它們是否嵌套),遞歸調用process.nextTick,將會沒完沒了,主線程根本不會去讀取事件隊列,致使阻塞後續調用,直至達到最大調用限制。
相比於在定時器中採用紅黑樹樹的操做時間複雜度爲0(lg(n)),而process.nextTick()
的時間複雜度爲0(1),相比之下更高效。
來舉一個複雜的栗子,這個栗子搞懂基本上就所有理解了:
setTimeout(() => { process.nextTick(() => console.log('nextTick1')) setTimeout(() => { console.log('setTimout1') process.nextTick(() => { console.log('nextTick2') setImmediate(() => console.log('setImmediate1')) process.nextTick(() => console.log('nextTick3')) }) setImmediate(() => console.log('setImmediate2')) process.nextTick(() => console.log('nextTick4')) console.log('sync2') setTimeout(() => console.log('setTimout2'), 0) }, 0) console.log('sync1') }, 0) // 輸出: sync1 nextTick1 setTimout1 sync2 nextTick2 nextTick4 nextTick3 setImmediate2 setImmediate1 setTimout2
process.nextTick()
,效率最高,消費資源小,但會阻塞CPU的後續調用;setTimeout()
,精確度不高,可能有延遲執行的狀況發生,且由於動用了紅黑樹,因此消耗資源大;setImmediate()
,消耗的資源小,也不會形成阻塞,但效率也是最低的。網上的帖子大多深淺不一,甚至有些先後矛盾,在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出~
參考:
Node——異步I/O
Node探祕之事件循環
Node探祕之事件循環--setTimeout/setImmediate/process.nextTick的差異
細說setTimeout/setImmediate/process.nextTick的區別
深刻淺出Nodejs
Node官方文檔
由setTimeout和setImmediate執行順序的隨機性窺探Node的事件循環機制
Node.js的event loop及timer/setImmediate/nextTick
Node.js 探祕:初識單線程的 Node.js | Taobao FED | 淘寶前端團隊
Node.js 事件循環機制 - 一像素 - 博客園
PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~
另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~