1. 程式人生 > >nodejs事件和事件迴圈詳解

nodejs事件和事件迴圈詳解

[toc] # 簡介 上篇文章我們簡單的介紹了nodejs中的事件event和事件迴圈event loop。本文字文將會更進一步,繼續講解nodejs中的event,並探討一下setTimeout,setImmediate和process.nextTick的區別。 # nodejs中的事件迴圈 雖然nodejs是單執行緒的,但是nodejs可以將操作委託給系統核心,系統核心在後臺處理這些任務,當任務完成之後,通知nodejs,從而觸發nodejs中的callback方法。 這些callback會被加入輪循佇列中,最終被執行。 通過這樣的event loop設計,nodejs最終可以實現非阻塞的IO。 nodejs中的event loop被分成了一個個的phase,下圖列出了各個phase的執行順序: ![](https://img-blog.csdnimg.cn/20200929155754234.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_25,color_8F8F8F,t_70) 每個phase都會維護一個callback queue,這是一個FIFO的佇列。 當進入一個phase之後,首先會去執行該phase的任務,然後去執行屬於該phase的callback任務。 當這個callback佇列中的任務全部都被執行完畢或達到了最大的callback執行次數之後,就會進入下一個phase。 > 注意, windows和linux的具體實現有稍許不同,這裡我們只關注最重要的幾個phase。 問題:phase的執行過程中,為什麼要限制最大的callback執行次數呢? 回答:在極端情況下,某個phase可能會需要執行大量的callback,如果執行這些callback花費了太多的時間,那麼將會阻塞nodejs的執行,所以我們設定callback執行的次數限制,以避免nodejs的長時間block。 # phase詳解 上面的圖中,我們列出了6個phase,接下來我們將會一一的進行解釋。 ## timers timers的中文意思是定時器,也就是說在給定的時間或者時間間隔去執行某個callback函式。 通常的timers函式有這樣兩種:setTimeout和setInterval。 一般來說這些callback函式會在到期之後儘可能的執行,但是會受到其他callback執行的影響。 我們來看一個例子: ~~~js const fs = require('fs'); function someAsyncOperation(callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // do someAsyncOperation which takes 95 ms to complete someAsyncOperation(() => { const startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { // do nothing } }); ~~~ 上面的例子中,我們呼叫了someAsyncOperation,這個函式首先回去執行readFile方法,假設這個方法耗時95ms。接著執行readFile的callback函式,這個callback會執行10ms。最後才回去執行setTimeout中的callback。 所以上面的例子中,雖然setTimeout指定要在100ms之後執行,但是實際上還要等待95 + 10 = 105 ms之後才會真正的執行。 ## pending callbacks 這個phase將會執行一些系統的callback操作,比如在做TCP連線的時候,TCP socket接收到了ECONNREFUSED訊號,在某些liunx作業系統中將會上報這個錯誤,那麼這個系統的callback將會放到pending callbacks中執行。 或者是需要在下一個event loop中執行的I/O callback操作。 ## idle, prepare idle, prepare是內部使用的phase,這裡就不過多介紹。 ## poll輪詢 poll將會檢測新的I/O事件,並執行與I / O相關的回撥,注意這裡的回撥指的是除了關閉callback,timers,和setImmediate之外的幾乎所有的callback事件。 poll主要處理兩件事情:輪詢I/O,並且計算block的時間,然後處理poll queue中的事件。 如果poll queue非空的話,event loop將會遍歷queue中的callback,然後一個一個的同步執行,知道queue消費完畢,或者達到了callback數量的限制。 因為queue中的callback是一個一個同步執行的,所以可能會出現阻塞的情況。 如果poll queue空了,如果程式碼中呼叫了setImmediate,那麼將會立馬跳到下一個check phase,然後執行setImmediate中的callback。 如果沒有呼叫setImmediate,那麼會繼續等待新來的callback被加入到queue中,並執行。 ## check 主要來執行setImmediate的callback。 setImmediate可以看做是一個執行在單獨phase中的獨特的timer,底層使用的libuv API來規劃callbacks。 一般來說,如果在poll phase中有callback是以setImmediate的方式呼叫的話,會在poll queue為空的情況下,立馬結束poll phase,進入check phase來執行對應的callback方法。 ## close callbacks 最後一個phase是處理close事件中的callbacks。 比如一個socket突然被關閉,那麼將會觸發一個close事件,並呼叫相關的callback。 # setTimeout 和 setImmediate的區別 setTimeout和setImmediate有什麼不同呢? 從上圖的phase階段可以看出,setTimeout中的callback是在timer phase中執行的,而setImmediate是在check階段執行的。 從語義上講,setTimeout指的是,在給定的時間之後執行某個callback。而setImmediate是在執行完當前loop中的 I/O操作之後,立馬執行。 那麼這兩個方法的執行順序上有什麼區別呢? 下面我們舉兩個例子,第一個例子中兩個方法都是在主模組中執行: ~~~js setTimeout(() =>
{ console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); ~~~ 這樣執行兩個方法的執行順序是不確定,因為可能受到其他執行程式的影響。 第二個例子是在I/O模組中執行這兩個方法: ~~~js const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); }); ~~~ 你會發現,在I/O模組中,setImmediate一定會在setTimeout之前執行。 ## 兩者的共同點 setTimeout和setImmediate兩者都有一個返回值,我們可以通過這個返回值,來對timer進行clear操作: ~~~js const timeoutObj = setTimeout(() =>
{ console.log('timeout beyond time'); }, 1500); const immediateObj = setImmediate(() => { console.log('immediately executing immediate'); }); const intervalObj = setInterval(() => { console.log('interviewing the interval'); }, 500); clearTimeout(timeoutObj); clearImmediate(immediateObj); clearInterval(intervalObj); ~~~ clear操作也可以clear intervalObj。 ## unref 和 ref setTimeout和setInterval返回的物件都是Timeout物件。 如果這個timeout物件是最後要執行的timeout物件,那麼可以使用unref方法來取消其執行,取消執行完畢,可以使用ref來恢復它的執行。 ~~~js const timerObj = setTimeout(() =>
{ console.log('will i run?'); }); timerObj.unref(); setImmediate(() => { timerObj.ref(); }); ~~~ > 注意,如果有多個timeout物件,只有最後一個timeout物件的unref方法才會生效。 # process.nextTick process.nextTick也是一種非同步API,但是它和timer是不同的。 如果我們在一個phase中呼叫process.nextTick,那麼nextTick中的callback會在這個phase完成,進入event loop的下一個phase之前完成。 這樣做就會有一個問題,如果我們在process.nextTick中進行遞迴呼叫的話,這個phase將會被阻塞,影響event loop的正常執行。 那麼,為什麼我們還會有process.nextTick呢? 考慮下面的一個例子: ~~~js let bar; function someAsyncApiCall(callback) { callback(); } someAsyncApiCall(() => { console.log('bar', bar); // undefined }); bar = 1; ~~~ 上面的例子中,我們定義了一個someAsyncApiCall方法,裡面執行了傳入的callback函式。 這個callback函式想要輸出bar的值,但是bar的值是在someAsyncApiCall方法之後被賦值的。 這個例子最終會導致輸出的bar值是undefined。 我們的本意是想讓使用者程式執行完畢之後,再呼叫callback,那麼我們可以使用process.nextTick來對上面的例子進行改寫: ~~~js let bar; function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1; ~~~ 我們再看一個實際中使用的例子: ~~~js const server = net.createServer(() => {}).listen(8080); server.on('listening', () => {}); ~~~ 上面的例子是最簡單的nodejs建立web服務。 上面的例子有什麼問題呢?listen(8000) 方法將會立馬繫結8000埠。但是這個時候,server的listening事件繫結程式碼還沒有執行。 這裡實際上就用到了process.nextTick技術,從而不管我們在什麼地方繫結listening事件,都可以監聽到listen事件。 ## process.nextTick 和 setImmediate 的區別 process.nextTick 是立馬在當前phase執行callback,而setImmediate是在check階段執行callback。 所以process.nextTick要比setImmediate的執行順序優先。 實際上,process.nextTick和setImmediate的語義應該進行互換。因為process.nextTick表示的才是immediate,而setImmediate表示的是next tick。 > 本文作者:flydean程式那些事 > > 本文連結:[http://www.flydean.com/nodejs-event-more/](http://www.flydean.com/nodejs-event-more/) > > 本文來源:flydean的部落格 > > 歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來