[譯] 你不知道的 Node

Pui發表於2019-03-04

在今年的 Forward.js 大會(一個 JavaScript 峰會),我進行了一場主題為“你不知道的 Node” 的演講,在那場演講中,我問了現場觀眾一系列關於 Node.js 執行時的問題,然而大部分搞技術的聽眾都不能全部回答得上。

我當時並沒有真的計算過,直到演講完了才有一些勇敢的人過來跟我坦白說他們不會。

這個問題正是讓我發表演講的原因,我並不認為我們教授 Node 的方式是對的。大多數關於 Nodejs 的教材內容主要集中在 Node 包和 Node 執行時之外的地方,大多數這些包都在 Node 執行時封裝好了模組(例如 httpstream),問題可能是藏在執行時裡面,然而你不懂 Node 執行時的話,你就麻煩了。

問題:大多數關於 Nodejs 的教材內容主要集中在 Node 包和 Node 執行時之外的地方。

我挑選了幾個問題並組織了一些答案來寫成這篇文章,答案就在問題的下面,建議嘗試先自己回答。

如果你發現了錯誤或誤導性的回答,請跟我聯絡。

問題 #1:什麼是呼叫棧?它是 V8 的一部分嗎?

呼叫棧百分之百就是 V8 的一部分,它是 V8 用來追蹤方法呼叫的資料結構。每一次我們呼叫一個方法,V8 在呼叫棧中放置一個該方法的引用,並且 V8 對每個其他方法的巢狀呼叫也這樣操作,同時也包括那些自身遞迴呼叫的方法。

Screenshot captured from my Pluralsight course — Advanced Node.js
Screenshot captured from my Pluralsight course — Advanced Node.js

當方法的巢狀呼叫結束時,V8 會逐個地將方法從棧中 pop 出來,並在它的位置使用方法的返回值。

為什麼這對於理解 Node 是如此關鍵?因為在每個 Node 程式中你只有一個呼叫棧。如果你令呼叫棧處於忙碌,你整個的 Node 程式也將變得忙碌。牢記這一點!

問題 #2:什麼是事件迴圈?它是 V8 的一部分嗎?

你覺得事件迴圈在這張圖的哪個部分?

Screenshot captured from my Pluralsight course — Advanced Node.js
Screenshot captured from my Pluralsight course — Advanced Node.js

答案是 libuv 。事件迴圈不是 V8 的一部分!

事件迴圈是操控外部事件並將它們轉換為回撥呼叫的實體,它是從事件佇列中取出事件並將事件的回撥函式推進呼叫棧的一個迴圈。並且該迴圈過程中分為多個獨立的階段。

如果這是你第一次聽說事件迴圈,這些概念對你可能幫助不大。事件迴圈是一副很大的輪廓圖的其中一部分:

Screenshot captured from my Pluralsight course — Advanced Node.js
Screenshot captured from my Pluralsight course — Advanced Node.js

你需要先理解這幅輪廓圖再理解事件迴圈,你需要先理解 V8 在這裡面飾演的角色、理解 Node APIs 並知道事件是怎樣進入佇列並被 V8 處理的。

Node APIs 是像 setTimeoutfs.readFile的一些方法,它們不是 JavaScript 本身的一部分,它們就是 Node 提供的方法。

事件迴圈在這張圖片的中間(一個更復雜的版本,真的)飾演一個組織者的角色。當 V8 呼叫棧為空的時候,事件迴圈可以決定接下來執行什麼。

問題 #3:當呼叫棧和事件迴圈佇列都為空時,Node 會做什麼?

Node 會直接退出。

當你執行一個 Node 程式時,Node 會自動地開始事件迴圈,當沒有事件處理時並且沒有其他任務時,Node 則會退出程式。

為了保持一個 Node 程式持續執行,你需要把一些任務放入事件佇列中。例如,當你建立一個計時器或一個 HTTP 伺服器時,你基本上就是在告訴事件迴圈要保持並檢測這些任務持續執行。

問題 #4:除了 V8 和 Libuv,Node 還有哪些外部依賴?

以下是一個 Node 程式可以使用的所有外部的庫:

  • http-parser
  • c-ares
  • OpenSSL
  • zlib

對 Node 本身來說,上面這些庫都是外部的,這些庫都有自己的原始碼、許可證,Node 只是使用它們而已。

你想記住它們是因為你想知道你的程式執行到哪裡了,如果你在做一些資料壓縮的工作,有可能是在 zlib 這個庫遇到問題,Node 是無辜的。

問題 #5:不用 V8 有可能執行一個 Node 程式嗎?

這可能是一個奇技淫巧的問題。你肯定是需要一個虛擬機器去執行 Node 程式,但 V8 並不是唯一的虛擬機器,你還可以使用 Chakra。

檢視這個 Github 倉庫來跟蹤 node-chakra 專案的進度:

問題 #6:module.exports 和 exports 兩者的區別?

你可以使用 module.exports 匯出模組的 API,你也可以使用 exports,但有個值得注意的地方:

module.exports.g = ...  // Ok

exports.g = ...         // Ok

module.exports = ...    // Ok

exports = ...           // Not Ok複製程式碼

為什麼?

exports 只是一個對 module.exports 的引用或別名,當你修改 exports 時你其實是在無意中試圖修改 module.exports,但修改對官方 API (即 module.exports)不會產生影響,你只是在模組作用域中得到一個區域性變數。

問題 #7:為什麼頂層變數不是全域性變數?

如果你在 module1 定義了一個頂層變數 g

// module1.js

var g = 42;複製程式碼

而你在 module2 依賴 module1並試圖訪問這個變數 g,你會得到錯誤 g is not defined

為什麼? 如果你在瀏覽器執行相同的操作,你可以在所有指令碼中訪問頂層定義的變數。

每個 Node 檔案在背後都有自己的 IIFE(立即呼叫函式表示式),所有在一個 Node 檔案中宣告的變數都被限制在這個 IIFE 的作用域中。

相關問題: 在一個 Node 檔案中只有下面這一行程式碼,執行它會輸出什麼:

// script.js</pre>

console.log(arguments);複製程式碼

你會看到一些引數!

為什麼?

因為 Node 執行的是一個函式。Node 將你的程式碼包裹在一個函式中,這個函式明確地定義了你上面看到的那 5 個引數。

問題 #8:exportsrequire、和 module三個物件在每個檔案中都是全域性可用的,但他們在每個檔案中又有區別,為什麼呢?

當你需要使用 require 物件時,你只是像使用全域性變數那樣直接使用它,然而,如果你在 2 個不同的檔案中比較 require 物件的區別,你會發現 2 個不同的物件,怎麼回事?

還是因為一樣的原因 IIFE(立即呼叫函式表示式):

正如你所見,IIFF 將以下 5 個引數傳遞到你的程式碼中:exports, require, module, __filename, and __dirname

當你在 Node 中使用這 5 個變數的時候似乎是在使用全域性變數,但它們只是函式引數。

問題 #9: Node 中的迴圈依賴是什麼?

如果你有一個 module1 依賴於 module2,而 module2 又反過來依賴於 module1,這將發生什麼?一個錯誤?

// module1

require('./module2');

// module2

require('./module1');複製程式碼

放心,不會報錯,Node 允許這樣做。

所以,module1 依賴於 module2,但因為 module2 又依賴於 module1,然而 module1 此時還沒就緒,module1 只會得到 module2 的不完整版本。

系統已經發出警告了。

問題 #10:什麼時候適合使用檔案系統的同步方法(像 readFileSync)?

每個 Node 中的 fs 方法都有一個同步版本,為什麼你要使用一個同步方法而不是一個非同步方法?

有時使用同步方法挺好的,舉個例子,可以在伺服器還在一直載入的時候,將同步方法用到任何初始化工作中。通常情況下,在初始化工作完成之後,你接下來的工作是根據獲得的資料繼續進行作業而不是引入回撥級別。使用同步方法是可以接受的,只要你使用的同步方法是一次性的。

然而,如果你在一個像是 HTTP 伺服器的 on-request 回撥函式裡使用同步方法,那就真的是 100% 錯誤!別那樣做。

我希望你能答上一部分或者所有的問題,以下是我寫得比較深入 Node.js 細節的文章:


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章