天天看點

nodejs事件和事件循環詳解簡介nodejs中的事件循環phase詳解setTimeout 和 setImmediate的差別process.nextTick

簡介

上篇文章我們簡單的介紹了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的執行順序:

nodejs事件和事件循環詳解簡介nodejs中的事件循環phase詳解setTimeout 和 setImmediate的差別process.nextTick

每個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執行的影響。 我們來看一個例子:

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操作之後,立馬執行。

那麼這兩個方法的執行順序上有什麼差別呢?

下面我們舉兩個例子,第一個例子中兩個方法都是在主子產品中運作:

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});           

這樣運作兩個方法的執行順序是不确定,因為可能受到其他執行程式的影響。

第二個例子是在I/O子產品中運作這兩個方法:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});           

你會發現,在I/O子產品中,setImmediate一定會在setTimeout之前執行。

兩者的共同點

setTimeout和setImmediate兩者都有一個傳回值,我們可以通過這個傳回值,來對timer進行clear操作:

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來恢複它的執行。

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呢?

考慮下面的一個例子:

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來對上面的例子進行改寫:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;           

我們再看一個實際中使用的例子:

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/

本文來源:flydean的部落格

歡迎關注我的公衆号:「程式那些事」最通俗的解讀,最深刻的幹貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!