更新:這篇文章如今是個人書《Node.js進階》的一部分。 在jscomplete.com/node-beyond…中閱讀此內容的更新版本以及有關Node.js的更多信息。node
在今年的Forward.js會議(關於JavaScript的會議)上,我分享了題爲「你不知道的NodeJS」的演講。 在那次演講中,我向觀衆提出了一系列有關Nodejs運行時的問題,大多數有技術背景的觀衆沒法回答其中大多數問題。git
我沒有真正去統計這個數據但確實能在會議室裏感受到。演講後一些有勇氣的人走近我而且認可了這個事實。github
這就是讓我發表演講的緣由。 我認爲咱們沒有以正確的方式教授Node.js! 關於Node.js的大多數學習內容都聚焦於Node包上,而不是它的運行時上。 大多數Node包將模塊封裝在自身的Node運行時中(例如http或stream)。 當你遇到問題時,這些問題多是在自身運行時發生,而且若是你不瞭解Node運行時,就會遇到麻煩。瀏覽器
我爲這篇文章精選一些問題和回答。列以下的標題中,能夠嘗試先鬧中回答它們。(若是你在這裏發現了錯誤或者有歧義的回答,請讓我知道)bash
調用確定是V8的一部分。它是V8用於保存函數調用軌跡的一種數據結構。每次咱們運行一個函數,V8都會將該函數的引用放入調用堆棧,並對該函數中嵌套的其餘函數進行相同的操做。這也包括遞歸調用的函數。服務器
當嵌套的函數運行結束,V8將一次彈出一個函數並用它的返回值替換它的位置。爲何這對於Node很重要? 由於每一個Node
進程僅得到一個調用堆棧。 若是使該調用堆棧處於繁忙狀態,則整個Node
進程都處於繁忙狀態。記住這一點。數據結構
你認爲下圖中的事件輪詢在哪裏?架構
事件輪詢由libuv
模塊提供,它不是V8的一部分。
事件輪詢是處理外部事件並將它們轉換回調函數運行的一種機制。這種輪詢會循環的從事件隊列中選擇事件執行,並將它們的回調函數推入調用堆棧中。異步
若是這是你第一次聽到事件循環,則這些定義不會有太大幫助。 事件循環只是更大架構下中的一部分:函數
你須要理解事件輪詢背後更大的架構、V8所扮演的角色、Node.js
的APIs以及知道這些事情是如何推入隊列並被V8執行的。
Node.js
APIs是像setTimeout
或fs.readFile
這樣的函數。這些並非JavaScript
的一部分,而由Node.js
提供的函數。
事件循環位於這張照片的中間(其實是它的一個更復雜的版本),而且像一個組織者。 當V8調用堆棧爲空時,事件循環能夠決定下一步執行什麼。
Node.js
會作什麼?它簡單的退出.
當你啓動一個Node.js
進程時,Node將自動啓動事件輪詢。當事件輪詢處於空閒狀態而且無其餘事件去處理時,程序將退出。
To keep a Node process running, you need to place something somewhere in event queues. For example, when you start a timer or an HTTP server you are basically telling the event loop to keep running and checking on these events. 爲了保持Node進程運行,你須要向事件隊列中放入一些內容。好比,當你能夠啓動一個定時器或者一個 HTTP
服務時,你就至關於告訴事件輪詢保持運行,同時去監聽一些事件。
如下是一個Node進程全部可使用的獨立庫:
zlib
庫底層堆棧遇到遇到麻煩,那麼你將面對一個zilb
相關的錯誤,而不是歸責於Node。這多是一個棘手的問題。 你確實須要一個VM來運行Node進程,可是V8並非惟一可使用的VM。 您可使用Chakra
。
module.exports
和exports
有什麼不一樣 ?你能夠一直使用module.exports
去導出模塊的API。除一種狀況外,你也可使用export
:
module.exports.g = ... // Ok
exports.g = ... // Ok
module.exports = ... // Ok
exports = ... // Not Ok
複製代碼
爲何?
export
僅僅是module.export
的一個別名或引用。 更改導出時,你將更改該引用,而再也不更改官方API(即module.exports)。 你只須要在模塊做用域中獲取局部變量便可。
若是你在模塊module1
中定義了一個頂級變量g
:
// module1.js
var g = 42;
複製代碼
同時你有一個模塊module2
引用了模塊module1
,而且嘗試訪問變量g
,你將獲得g is not defined.
的錯誤。
爲何?
若是你在瀏覽器端作相同的事情,你能夠在該定義該頂級變量腳本以後的全部腳本里訪問該頂級變量。 每一個Node文件在後臺都有其本身的IIFE(函數調用表達式)。 在Node文件中聲明的全部變量都做用於該IIFE。
相關問題: 下面這個僅僅含有一行代碼的Node文件將會輸出什麼?
// script.js
console.log(arguments);
複製代碼
你將會看一些參數!
爲何?
由於Node執行的是一個函數。Node將你的代碼包裝到一個函數中。該函數中明肯定義了上圖中你所見的5個參數。
export
、require
、 module
都是全局可用的,然而在它們在每一個文件又有所不一樣,爲何?當你須要使用require
對象,你就是像一個全局變量那樣直接使用它。然而,若是你在兩個不一樣的文件中檢查require
,你將看到兩個不一樣的對象。爲何? 由於 因爲具備相同的IIFE魔法:
如你所見, 「這IIFE魔法」向你的代碼中傳遞如下五個參數: exports
, require
, module
, __filename
, 和 __dirname
。當你在Node中使用這5個參數時,它們看起來像全局變量,但實際上它們僅僅是函數參數。
若是你定義一個模塊module1
引用了模塊module2
,同時模塊module2
內部又引用了模塊module1
。將發生什麼?報錯?
// module1
require('./module2');
// module2
require('./module1');
複製代碼
你不會獲得報錯。由於Node容許那種狀況。 因此模塊module1
引用模塊module2
,可是由於模塊module2
依賴模塊module1
且模塊module1
沒有加載完成,模塊module1
將僅僅獲取到模塊module2
的一個部分版本。程序將提示警告。
readFileSync
)?Node模塊fs
中的每個方法都有一個同步版本。爲何你會使用一個同步方法代替一個異步方法?
有時候,使用同步方法會更好。好比服務器仍在加載時,能夠在任何初始化步驟中使用它。 一般狀況下,初始化步驟以後執行的全部操做都取決於在那裏獲取的數據。 只要你使用同步方法是一次性的,就可使用同步方法來避免引入回調獄。
可是,若是您在處理程序(例如HTTP服務器請求回調)中使用同步方法,那簡直就是100%錯誤。 不要那樣作。
我但願你可以回答以上部分或者所有的問題。
感謝閱讀。