1. 程式人生 > >問答方式學 Node.js(二)

問答方式學 Node.js(二)

Q: 好了,我們繼續談談 Node.js 吧,我記得上次說到了「非阻塞」和「事件驅動」,這引起了我的好奇心,但是又給我潑了一桶冷水,這兩個詞有點高階。

A:別急,我們先來看一個簡單的場景:

人人網刷朋友新鮮事你應該用過吧?實現這樣的功能有一種簡單的方式,是讓使用者與伺服器之間保持一個長輪詢。

但是它與普通的 Ajax 不一樣,伺服器不會立馬返回資訊,它會先 hold 住,等待應該返回資訊了,它才會返回資訊(比如你的好友發了一條新的狀態)。

從傳統服務端來看(比如 Apache),每次一個新使用者連到你的網站上,你的伺服器得新開一個連線,每個連線都需要佔用一個執行緒,這些執行緒大部分都是閒著的(比如等你的好友髮狀態,查資料庫等),雖然它們是閒著的,但是照樣佔用了記憶體,也就是說,如果使用者達到一定的規模,伺服器的記憶體就會耗光而癱瘓。

解決辦法有很多,比如說使用執行緒池,但是它依然是阻塞的,如果執行緒池裡的所有執行緒都被阻塞(網速慢,被人惡意暫用)那麼接下來的請求將會排隊等待。

Node.js 就不相同了,它使用了「非阻塞」與「事件驅動」模型,你可以把它想象成一個 Event Loop 迴圈,這個迴圈會一直跑。一個新的請求來了,Event Loop 接收這個請求,然後交給其他執行緒,比如查詢資料庫,然後響應一個 callback,接著接收其他請求,而不是等待資料庫結果的返回。

如果資料庫返回了結果,服務端將會把它返回給客戶端,並繼續迴圈。這就是事件驅動:服務端只在有事情發生時,才會有相應的處理(或者是接受請求,或者是一些 callback)。

Q:這麼看來,Node.js 的非阻塞和事件驅動,是基於這個 Event Loop 的?

A:是的,簡單來講,Node.js 的 Event Loop 是基於 libuv,而瀏覽器的 Event Loop 則是在 html5 規範 中定義,具體實現交給瀏覽器廠商。

Q:有趣了,還有兩種 Event Loop。

A:對比來看,它們有點相似:

在瀏覽器中比較簡單,值得注意的一點是,會在每個 tasks 之後,會把當前 microtask 佇列裡的任務都執行完:

Node.js 稍微複雜一點,每次 Event Loop 都需要經過六個階段,每一個階段之後,都會執行 nextTick、microtasks (resolved promise, 等):

   ┌───────────────────────┐
┌─>│        timers         │ <─── setTimeout/setInterval callback
│  └──────────┬────────────┘      ┌─────────────────────────┐
│             │                   │ nextTick queue          │
│             │ <───────────────  │                         │
│             │                   │ microTask queue         │
│  ┌──────────┴────────────┐      └─────────────────────────┘
│  │     I/O callbacks     │
│  └──────────┬────────────┘      ┌─────────────────────────┐
│             │                   │ nextTick queue          │
│             │ <───────────────  │                         │
│             │                   │ microTask queue         │
│  ┌──────────┴────────────┐      └─────────────────────────┘
│  │     idle, prepare     │ <─── 僅內部使用
│  └──────────┬────────────┘
│             │                   ┌─────────────────────────┐
│             │                   │ nextTick queue          │
│             │ <──────────────── │                         │
│             │                   │ microTask queue         │
│             │                   └─────────────────────────┘
│             │                   ┌─────────────────────────┐
│  ┌──────────┴────────────┐      │      incoming:          │
│  │         poll          │ <────┤      connections,       │
│  └──────────┬────────────┘      │      data, etc          │
│             │                   └─────────────────────────┘
│             │                   ┌─────────────────────────┐
│             │                   │ nextTick queue          │
│             │ <──────────────── │                         │
│             │                   │ microTask queue         │
│             │                   └─────────────────────────┘
│  ┌──────────┴────────────┐
│  │        check          │ <─── setImmediate callback
│  └──────────┬────────────┘      ┌─────────────────────────┐
│             │                   │ nextTick queue          │
│             │ <───────────────  │                         │
│             │                   │ microTask queue         │
│  ┌──────────┴────────────┐      └─────────────────────────┘
│  │    close callbacks    │ <─── eg: socket.on("close",func)
│  └──────────┬────────────┘      ┌─────────────────────────┐
│             │                   │ nextTick queue          │
│             │ <───────────────  │                         │
└─────────────┴                   │ microTask queue         │
                                  └─────────────────────────┘
複製程式碼

來一段簡單的程式碼,猜猜瀏覽器(Chrome)和 Node.js 分別輸出什麼:

console.log('start');

setTimeout(() => {
  console.log('timer1');
  Promise.resolve().then(() => {
      console.log('promise1');
  });
}, 0);

setTimeout(() => {
  console.log('timer2');
  Promise.resolve().then(() => {
    console.log('promise2');
  });
}, 0);

console.log('end');
複製程式碼

Q:瀏覽器 (Chrome) 中肯定是輸出 start、end、time1、promise1、time2、promise2,至於 Node.js 中,我猜也是一樣的吧?

A:我們先來驗證一下:

瀏覽器中:

start
end
timer1
promise1
timer2
promise2
複製程式碼

Node.js 中:

start
end
timer1
timer2
promise1
promise2
複製程式碼

看來和想象中的不一樣,別急,看個動圖就會明白了:

瀏覽器中:

Node.js 中:

Q:原來是這樣,那如果在每個 setTimeout callback 里加上 process.nextTick 那麼是比 Promise.then 先執行?

A:是的,還記得上面所說過的嗎,在每個階段後都會執行 nextTick queue 以及 micktasks queue,nextTick queue 的優先順序比 micktasks queue 高。

Q:我懂了。對了,我記得你提到了 libuv,它是什麼?不是說 Node.js 使用的是 v8 嗎,它和 v8 又有什麼關係?

A:...

未完待續...

其他

參考