Node中的事件循環

Node中的事件循環

若是對前端瀏覽器的時間循環不太清楚,請看這篇文章。那麼node中的事件循環是什麼樣子呢?其實官方文檔有很清楚的解釋,本文先從node執行一個單文件提及,再講事件循環。html

node的內部模塊

任何高級語言的存在都有必定的執行環境,好比瀏覽器的代碼是在瀏覽器引擎中,那麼在node環境中也有必定的執行環境。咱們先來看一下官網的依賴包有哪些?前端

  • V8
  • libuv
  • http-parser
  • c-cares
  • OpenSSL
  • zlib

上面就是nodejs中依賴的模塊。那麼這些模塊之間是如何工做的呢?模塊之間的工做關係以下圖所示:node


主要過程以下:git

  • step1: 用戶的代碼經過v8引擎解釋器,解析爲兩部分:"當即執行"和"異步執行"。

當即執行:能夠理解爲,須要v8引擎去處理的代碼;
異步執行:並非真正的異步,能夠理解爲,不須要v8引擎處理的和須要異步處理的。github

  • step2: 「異步執行」的部分,經過v8引擎和底層之間創建的綁定關係,去執行對應的操做
  • step3: 在「異步執行」部分,經過libuv內部的事件循環機制,無阻塞調用。libuv在執行的時候,主要經過handles和request實現對應的操做,handles和requests具有不一樣的數據結構。官網解釋,handles是長期存在的對象,request是短時間存在的對象,猜想來說,requests和handles有不一樣的垃圾回收機制。

libuv的事件循環

一個線程有惟一的一個事件循環(event loop)。線程非安全。

這裏須要理解兩點:web

  • 線程

這可能和咱們理解的不太同樣,Javascript代碼是單線程的,可是libuv不是單線程的,他能夠開啓多個線程,libuv 提供了一個調度的線程池,線程池中的線程數目,默認是4個,最多1024個(爲何?由於每個線程都會佔用資源,而內存是有限的),關於線程池的能夠看官方文檔。瀏覽器

  • 線程安全

對數據的操做無非就是讀和寫,線程安全,簡單來講,就是一個線程對這一份數據具備獨佔性,只有當該線程操做完成,其餘線程才能夠進行操做,固然線程安全的概念遠不止這些,詳細能夠看維基百科,這裏就簡單理解一下就好了。安全

libuv中的事件循環

事件循環圖,以下所示:網絡

主要分爲下面幾步:數據結構

  • step1: 線程啓動時,初始化一個時間:now,爲了計算後面的timer的回調函數何時執行
  • step2: 判斷事件循環是否存活,若是不存活,當即退出,不然進行下一步。判斷是否存活的依據:索引是否存在。索引就是指否還有須要執行的事件,是否還有請求,關閉事件循環的請求等等。(用白話來說,就是看還有沒有沒處理的事情)
  • step3: 執行全部的定時器(timers)在事件循環以前
  • step4: 執行待執行(pending)的回調,通常的IO輪詢都會在輪詢後,當即執行,可是有的也會延遲(defer)執行,延遲執行的,就會在這個階段執行
  • step4: 執行空閒(idle)函數,每一個階段都會執行的,通常狀況下是執行一些必要的操做,程序內置的
  • step5: 執行準備好的回調函數,具體內部使用的
  • step6: IO輪詢執行,直到超時,在阻塞執行以前,會計算超時時間,也就是中止輪詢的時間:

    • 若是隊列爲空、或者是即將關閉,或者有將要關閉的handles,timeout爲0
    • 若是沒有上面的狀況,超時時間就取最近的timer時間,不然就是無窮大

(用白話來理解,就是看有沒有要關閉的,有的話,就直接往下走,沒有的話,看看有哪一個事件比較急,到了點就去執行)

  • step7: 執行IO
  • step8: 檢查接下來要執行哪些handle,保證正確執行
  • step9: 是否存在關閉的回調,若是有就執行,關閉循環,不然繼續循環

一般狀況下來說,文件的I/O會調用線程池,可是網絡請求的I/O老是用同一個線程。

Node中的事件循環

阻塞和非阻塞

node中全部的代碼幾乎都提供了同步(阻塞)和異步(非阻塞)的方式,你能夠選擇使用哪種方式,可是不要混合使用

node中的事件循環,就是一個簡版的libuv事件循環機制圖

NodeJs中的定時器

NodeJs中的定時器主要有三種:

  • setTimeout
  • setInterval
  • setImmediate

三個定時器都有對應的取消函數:

  • clearTimeout
  • clearInterval
  • clearImmediate

setTimeout && setInterval

setTimeout和setInterval行爲和在瀏覽器環境中的行爲相似,可是setTimeout和setImmediate有一點不一樣。在libuv中能夠看到,判斷循環是否結束的時候,是須要判斷是否還有待執行的函數,若是隻剩下一個setTimeout或者setInterval函數,那麼整個循環還會繼續存在,node提供了一個函數,可讓循環暫時休眠

  • unref
  • ref

unref是可讓setTimeout暫時休眠,ref能夠再次喚醒

setImmediate

setImmediate是指定在事件循環結束執行的。主要發生在poll階段以後

若是poll隊列沒空,則一直執行,直到對列空位置

若是poll隊列空了,有setImmediate事件,則會跳到check階段

若是poll隊列空了,沒有setImmediate事件,就會查看哪個timer事件快要到期了,轉到timers階段

依據上面的解釋,就有了setTimeout和setImmediate執行前後順序的問題:

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

先說答案:

可能會有兩種狀況:
timeout
immediate
或者
immediate
timeout

爲何?
主要是setTimeout在前或者後的問題,依賴於線程的執行速度。
主要是兩個階段:

  • 一、v8引擎執行環境掃描代碼,啓動事件循環,當走到setTimeout的時候,會將timeout丟進libuv事件隊列中
  • 二、v8引擎繼續執行,走到setImmediate

    • 此時,上面的libuv事件隊列可能執行第一次,剛走到poll階段,那麼接下來就會打印immediate,
    • 也可能libuv事件隊列,已經第二次循環,通過了poll階段,而後判斷timeout到時間了,去執行timeout了,這樣就會先打印timeout而後再打印immediate

因此根本緣由是在於事件循環執行了一次仍是兩次。

那咱們接下來看看事件循環的邏輯

nextTick

Node添加了這樣一個API,這個並不在事件循環的機制內,可是和時間循環機制相關。先來看一下定義:

nextTick的定義是在事件循環的下一個階段以前執行對應的回調。

雖然nextTick是這樣定義的,可是它並非爲了在事件循環的每一個階段去執行的。
主要有下面兩種應用場景:

  • 做爲下一個執行階段的鉤子,去清理不須要的資源,或者再次請求
  • 等運行環境準備好以後,再去執行回調

案例一:

let bar;

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

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

bar = 1;

// 輸出
undefined
1

輸出undefine的狀況是,由於執行函數的時候,bar並無被賦值,而process.nextTick則能保證整個執行環境都準備好了再去執行

案例二:

const server = net.createServer();
server.on('connection', (conn) => { });

server.listen(8080);
server.on('listening', () => { });

當v8引擎執行完代碼後,listen的回調會直接命中poll階段,那麼server的connect事件就不會執行

案例三:

想要在構造函數中,去發送對應的事件,由於此時v8引擎尚未掃描到,而構造函數的代碼會當即執行,就須要nextTick

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
  // 這樣操做無效
  this.emit('event');
  // 應該這樣
  // process.nextTick(() => {
    this.emit('event');
  });
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});

總結

上面三個案例,重點在於v8引擎是單線程當即執行,而libuv則是異步執行,想要在異步循環以前執行一些操做就須要process.nextTick

參考文檔

Node官網解釋
libuv的設計
關於libuv的概念詳細解釋
libuv線程池實現
併發

相關文章
相關標籤/搜索