Node中的事件循環

Node中的事件循環

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

node的內部模塊

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

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

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


主要過程如下:

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

立即執行:可以理解爲,需要v8引擎去處理的代碼;
異步執行:並不是真正的異步,可以理解爲,不需要v8引擎處理的和需要異步處理的。

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

libuv的事件循環

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

這裏需要理解兩點:

  • 線程

這可能和我們理解的不太一樣,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在前或者後的問題,依賴於線程的執行速度。
主要是兩個階段:

  • 1、v8引擎執行環境掃描代碼,啓動事件循環,當走到setTimeout的時候,會將timeout丟進libuv事件隊列中
  • 2、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線程池實現
併發

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章