首頁  >  文章  >  web前端  >  深入淺析nodejs中的事件和事件循環

深入淺析nodejs中的事件和事件循環

青灯夜游
青灯夜游轉載
2021-04-14 11:06:122163瀏覽

這篇文章跟大家講解一下nodejs中的event,並探討setTimeout,setImmediate和process.nextTick的差異。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有幫助。

深入淺析nodejs中的事件和事件循環

nodejs中的事件循環


#雖然nodejs是單執行緒的,但nodejs可以將操作委託給系統內核,系統內核在後台處理這些任務,當任務完成之後,通知nodejs,從而觸發nodejs中的callback方法。

這些callback會被加入輪循佇列中,最後被執行。

透過這樣的event loop設計,nodejs最終可以實現非阻塞的IO。 【相關推薦:《nodejs 教學》】

nodejs中的event loop被分成了一個個的phase,下圖列出了各個phase的執行順序:

#每個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(&#39;fs&#39;);function someAsyncOperation(callback) {  // Assume this takes 95ms to complete  fs.readFile(&#39;/path/to/file&#39;, 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 completesomeAsyncOperation(() => {  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(&#39;timeout beyond time&#39;);
}, 1500);

const immediateObj = setImmediate(() => {
  console.log(&#39;immediately executing immediate&#39;);
});

const intervalObj = setInterval(() => {
  console.log(&#39;interviewing the interval&#39;);
}, 500);

clearTimeout(timeoutObj);
clearImmediate(immediateObj);
clearInterval(intervalObj);

clear操作也可以clear intervalObj。

unref 和 ref

setTimeout和setInterval返回的对象都是Timeout对象。

如果这个timeout对象是最后要执行的timeout对象,那么可以使用unref方法来取消其执行,取消执行完毕,可以使用ref来恢复它的执行。

const timerObj = setTimeout(() => {
  console.log(&#39;will i run?&#39;);
});

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(&#39;bar&#39;, 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(&#39;bar&#39;, bar); // 1
});

bar = 1;

我们再看一个实际中使用的例子:

const server = net.createServer(() => {}).listen(8080);

server.on(&#39;listening&#39;, () => {});

上面的例子是最简单的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的博客

更多编程相关知识,请访问:编程视频!!

以上是深入淺析nodejs中的事件和事件循環的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除