什麼是CommonJS規範?它與NodeJS是什麼關係呢?CommonJS 的底層實現原理是什麼? ...
1. CommonJS模塊規範
1.1 模塊引用
var math = require('math');
1.2 模塊定義
[!NOTE]
上下文提供exports對象用於導出當前模塊的方法和變數,並且他是唯一的導出出口
exports實際上是module.exports,而module.exports就是以一個暴露給外部的對象。
- exports.some就是給這個對象上添加屬性
- 直接使用 module.exports = {...} 則可以讓外部直接獲取到這個對象,相當與為exports換了一個引用,如果在這之前使用exports.some會把之前的覆蓋
1.3 CommonJS 用法
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
1.4 原理
var module = require('./a.js')
module.a
// 這裡其實就是包裝了一層立即執行函數,這樣就不會污染全局變數了,
// 重要的是 module 這裡,module 是 Node 獨有的一個變數
module.exports = {
a: 1
}
// module 基本實現
var module = {
id: 'xxxx', // 我總得知道怎麼去找到他吧
exports: {} // exports 就是個空對象
}
// 這個是為什麼 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 導出的東西
var a = 1
module.exports = a
return module.exports
};
// 然後當我 require 的時候去找到獨特的
// id,然後將要使用的東西用立即執行函數包裝下,over
2. Node的模塊實現
在Node中引入模塊,需要經歷3個步驟
- 路徑分析
- 文件定位
- 編譯執行
在node中,模塊分為兩類:一類是node提供的模塊稱為核心模塊,一類是用戶編寫的成為文件模塊。
核心模塊在編譯中編譯成了二進位文件。在Node進程啟動時,部分核心模塊就被直接載入入記憶體。所以這部分核心模塊引入時就省了文件定位和編譯執行這兩個步驟,並且在路徑分析中優先判斷,它的載入速度是最快的。
文件模塊是運行時動態載入。需要完整的路徑分析、文件定位、編譯執行
2.1 優先從緩存載入
Node對引入的模塊都回進行緩存,而且緩存的是編譯執行後的對象。
不管是核心模塊還是文件模塊,require()都一律採用緩存優先的方式。
2.2 路徑分析和文件定位
2.2.1 模塊標識符分析
- 核心模塊
- 路徑形式的文件模塊
- 自定義模塊
- node_modules下
- 查找最費時
2.2.2 文件定位
- 文件拓展名分析
- 如果省略拓展名,回按 .js .node .json的次序依次嘗試
- 如果.node .json的話,加上拓展名會加快一點速度
- 同步配合緩存,可大幅緩解單線程中阻塞式調用的缺陷
- 目錄分析和包
- 如果沒有文件名,會將Index當作預設文件名
2.3 模塊編譯
- .js文件
- 通過fs同步讀取後編譯執行
- .node
- 這是用C/C++編寫的拓展文件,通過dlopen()方法載入最後編譯生成的文件
- .json
- 用JSON.parse()解析返回結果
- 其餘拓展名
- 當作.js文件處理
[!NOTE]
每一個編譯成功的模塊都會將其文件路徑索引緩存在Module._cache對象上,以提高二次引入性能
2.3.1 js模塊的編譯
[!NOTE]
在編譯的過程中,Node對獲取的JS文件進行了頭尾包裝。這也是每個模塊都能訪問到 require、exports、module、__filename、__dirname的原因
(funciton(exports, require, module, __filename, __dirname) {
/* 自己寫的代碼 */
});
這樣使得模塊文件間都進行了作用域隔離,不用擔心變數污染全局。
為moudle.exports賦值,exports對象是通過形參的方式傳入,直接賦值形參會改變形參的引用,但並不能改變作用域外的值。
exports = function() {
// my class
}
var change = function(a) {
a = 100;
}
var a = 10;
change(a);
console.log(a); // => 10
如果要達到require引入一個類的效果,請賦值給 module.exports對象。這個迂迴的方案不改變形參的引用。
2.3.2 C/C++ 模塊的編譯
Node調用process.dlopen()方法進行載入和執行。
實際上 .node模塊並不需要編譯,因為它是編寫C/C++模塊之後編譯生成的,所以這裡只有載入和執行的過程。在執行的過程中,模塊exports對象與.node模塊產生練習,然後返回給調用者。
3. 核心模塊
[!NOTE]
Node的核心模塊在編譯成可執行文件的過程中被編譯進了二進位文件。核心模塊其實分為C/C++編寫的和Javascript編寫的兩部分,其中C/C++文件存放在Node項目的src目錄下,Javascript文件存放在lib目錄下。
C/C++拓展模塊
模塊調用棧
前後端公用模塊
- 模塊側重點
前端瓶頸在於帶寬,後端瓶頸在於CPU和記憶體等資源。前端需要通過網路載入代碼,後端則從磁碟載入,二者載入速度不再同一量級上。
node的模塊引入幾乎都是同步的,但前端模塊若是也採用同步方式來引入必會在用戶體驗上造成很大的問題,即UI初始化實際過長
4. AMD規範
Asynchronous Moudle Definition “非同步模塊定義”, AMD需要在聲明的時候指定所有的依賴,通過形參傳遞依賴到模塊內容中。
定義如下
define(id?, dependencies, factory);
5. CMD 規範
與AMD主要區別在於定於模塊與依賴引入部分。
CMD支持動態引入
define(funtion(require, exports, moudle) {
// The module code goes here
})