Node.js 原理簡介


Node.js 的官方文檔中有一段對 Node.js 的簡介,如下。

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.

大意就是說 Node.js 是基於 V8 的 JavaScript 運行時,事件驅動、非阻塞,因此輕量、高效。

寥寥數語,並沒有說清楚 Node.js 到底是什么。參考了一些 Node.js 的官方文章以及社區里的分析,整理如下。

基礎架構

要想深入理解 Node.js,我們需要把 Node.js 進行必要的拆解,了解每個組成部分的作用,它們之間如何交互,最終構成 Node.js 這個強大的運行時環境。

image

上圖是 Node.js 的內部結構圖。我們可以看到,自底向上主要可以分成三層:最底層是 Node.js 依賴的各種庫,有 V8、libuv 等;中間層是各種 Binding,也就是膠水代碼;最上層是應用代碼,可使用 Node.js 的各種 API。

  • V8
    Google 開源的高性能 JavaScript 引擎,它將 JavaScript 代碼轉換成機器碼,然后執行,因此速度非常快。V8 以 C++ 語言開發,Google 的 Chrome 瀏覽器正是使用的 V8 引擎。

  • libuv
    libuv 以 C 語言開發,內部管理着一個線程池。在此基礎之上,提供事件循環(Event Loop)、異步網絡 I/O、文件系統 I/O等能力。

  • 其他底層依賴庫
    c-arescrypto (OpenSSL)http-parser 以及 zlib。這些依賴提供了對系統底層功能的訪問,包括網絡、壓縮、加密等。

Node.js 底層的依賴庫,有的以 C 語言開發,有的以 C++ 語言開發,如何讓應用代碼(JavaScript)能夠與這些底層庫相互調用呢?這就需要中間層的 Binding 來完成。Binding 是一些膠水代碼,能夠把不同語言綁定在一起使其能夠互相溝通。在 Node.js 中,binding 所做的就是把 Node.js 那些用 C/C++ 寫的庫接口暴露給 JS 環境。

中間層中,除了 Binding,還有 Addon。Binding 僅橋接 Node.js 核心庫的一些依賴,如果你想在應用程序中包含其他第三方或者你自己的 C/C++ 庫的話,需要自己完成這部分膠水代碼。你寫的這部分膠水代碼就稱為 Addon。本質上都是完成橋接的作用,使得應用與底層庫能夠互通有無。

應用層的代碼,就不必多言了,我們開發的應用、npm 安裝的包都運行在這里。

事件循環 (event loop)

剛接觸 Node.js 的時候,就知道 Node.js 有一個事件循環,類似於 while(true),但是不知道每次循環什么時候開始,什么時候結束,在每次循環中,Node.js 是如何處理同步與異步代碼的。

要說事件循環,就不得不先說明一下 Node.js 的工作流程。下圖可以簡要說明。

QRePV

一個 Node.js 應用啟動時,V8 引擎會執行你寫的應用代碼,保持一份觀察者(注冊在事件上的回調函數)列表。當事件發生時,它的回調函數會被加進一個事件隊列。只要這個隊列還有等待執行的回調函數,事件循環就會持續把回調函數從隊列中拿出並執行。

在回調函數執行過程中,所有的 I/O 請求都會轉發給工作線程處理。libuv 維持着一個線程池,包含四個工作線程(默認值,可配置)。文件系統 I/O 請求和 DNS 相關請求都會放進這個線程池處理;其他的請求,如網絡、平台特性相關的請求會分發給相應的系統處理單元進行處理。

安排給線程池的這些 I/O 操作由 Node.js 的底層庫執行,完成之后觸發相應事件,對應的事件回調函數會被放入事件隊列,等待執行后續操作。這就是一個事件在 Node.js 中執行的整個生命周期。

前面說了,我們只知道 Node.js 有事件循環,但是不知道每次循環何時開始、何時結束。下面就簡要說明一下每次循環的處理過程,詳細內容請參考Node.js 官方說明

一次事件循環,大概可以分為如下幾個階段:
image

圖中每一個方塊,在事件循環中被稱為一個階段(phase)。

每個階段都有自己獨有的一個用於執行回調函數的 FIFO 隊列。當事件循環進入一個指定階段時,會執行隊列中的回調函數,當隊列中已經被清空或者執行的回調函數個數達到系統最大限制時,事件循環會進入下一個階段。

上圖中總共有6個階段:

  • timers: 該階段執行由 setTimeout()setInterval() 設置的回調函數。
  • I/O callbacks: 執行除了close 回調、timers 以及
    setImmediate() 設置的回調以外的幾乎所有的回調。
  • idle,prepare: 僅供內部使用。
  • poll: 檢索新的 I/O 事件;在適當的時候 Node.js 會阻塞等待。
  • check: 執行 setImmediate() 設置的回調。
  • close callbacks: 執行關閉回調。比如: socket.on('close', ...).

這里有個令人困惑的地方,I/O callbackspoll 這兩個階段有什么區別? 既然 I/O callbacks 中已經把回調都執行完了,還要 poll 做什么?

查閱了 libuv 的文檔后發現,在 libuv 的 event loop 中,I/O callbacks 階段會執行 Pending callbacks 。絕大多數情況下,在 poll 階段,所有的 I/O 回調都已經被執行。但是,在某些情況下,有一些回調會被延遲到下一次循環執行。也就是說,在 I/O callbacks 階段執行的回調函數,是上一次事件循環中被延遲執行的回調函數。

還需要提到的一點是 process.nextTick()process.nextTick() 產生的回調函數保存在一個叫做 nextTickQueue 的隊列中,不在上面任何一個階段的隊列里面。當當前操作完成后,nextTickQueue 中的回調函數會立即被執行,不管事件循環處在哪個階段。也就是說,在 nextTickQueue 中的回調函數被執行完畢之前,事件循環不會往前推進。

測試與實踐

如下代碼中使用了 setTimeout(), setInterval(), setImmediate(), promise, process.nextTick(),可借助於輸出結果,理解事件循環。

'use strict';

const fs = require('fs');

console.log('script start');

const interval = setInterval(() => {  
  console.log('setInterval')
}, 500);

setTimeout(() => {  
  console.log('setTimeout 1');
  Promise.resolve().then(() => {
    console.log('promise 3');
  }).then(() => {
    console.log('promise 4');
    process.nextTick(() => {
      console.log('nextTick 1');
    });
  }).then(() => {
    setTimeout(() => {
      console.log('setTimeout 2');
      Promise.resolve().then(() => {
        console.log('promise 5');
      }).then(() => {
        console.log('promise 6');
        process.nextTick(() => {
          console.log('nextTick 2');
        });
      }).then(() => {
        clearInterval(interval);
      });
    }, 0);
  });
}, 1000);

Promise.resolve().then(() => {  
  console.log('promise 1');
}).then(() => {
  console.log('promise 2');
});

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

console.log('script done');

執行結果為:

script start
script done
promise 1
promise 2
setImmediate 1
setInterval
setTimeout 1
promise 3
promise 4
nextTick 1
setInterval
setTimeout 2
promise 5
promise 6
nextTick 2


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM