在 NodeJS 中有一個方法是我們使用頻率最高的,那就是 require 方法。NodeJs 遵循 CommonJS 規范,該規范的核心是通過 require來加載其他依賴的模塊。
幾個問題
- module.exports 或者 exports 是全局變量嗎?
- 模塊的加載是同步還是異步?
- 循環引用會不會產生性能問題或者導致錯誤?
什么是 CommonJS
每一個文件就是一個模塊,擁有自己獨立的作用域,變量,以及方法等,對其他的模塊都不可見。CommonJS 規范規定,每個模塊內部,module 變量代表當前模塊。這個變量是一個對象,它的 exports 屬性(即module.exports
)是對外的接口。
Node 模塊的分類
- build-in modules —— Nodejs 中以 C++ 形式提供的模塊。
- constant module —— Nodejs 中定義常量的模塊。
- native module —— Nodejs 中以 javascript 形式提供的模塊。
- 第三方module —— 由第三方提供的模塊。
module 對象
NodeJs 內部提供一個 Module 構建函數。所有模塊都是 Module 的實例。
每個模塊內部,都有一個 module 對象,代表當前模塊。它有以下屬性。
-
module 對象的屬性
module.id
模塊的識別符,通常是帶有絕對路徑的模塊文件名。module.filename
模塊的文件名,帶有絕對路徑。module.loaded
返回一個布爾值,表示模塊是否已經完成加載。module.parent
返回一個對象,表示調用該模塊的模塊(程序入口文件的module.parent為null)module.children
返回一個數組,表示該模塊要用到的其他模塊。module.exports
表示模塊對外輸出的值。
-
module.exports 屬性
module.exports
屬性表示當前模塊對外輸出的接口,其他文件加載該模塊,實際上就是讀取module.exports
變量。module.exports
屬性表示當前模塊對外輸出的接口,其他文件加載該模塊,實際上就是讀取module.exports
變量。 -
exports 變量
我們有時候會這么寫:
// test.js
function test(){
console.log(test);
}
export.test = test;
// result.js
const test = require("./test")
這樣也可以拿到正確的結果,這是因為:exports 變量指向 module.exports。這等同在每個模塊頭部,有一行這樣的命令。
var exports = module.exports;
注意:不能直接給 exports 變量賦值,這樣會改變 exports 的指向,不再指向 module.exports。在其他模塊使用 require 方法是拿不到賦給 exports 的值的,因為 require 方法獲取的是其他模塊的 module.exports 的值。
建議:盡可能的使用 module.exports
來導出結果。
模塊的流程
- 創建模塊
- 導出模塊
- 加載模塊
- 使用模塊
require 方法
require 是 node 用來加載並執行其它文件導出的模塊的方法。
在 NodeJs 中,我們引入的任何一個模塊都對應一個 Module 實例,包括入口文件。
完整步驟:
- 調用父模塊的 require 方法(父模塊是指調用模塊的當前模塊)
require = function require(path) {
return mod.require(path);
};
-
調用 Module 的 _load 方法
-
通過
Module._resolveFilename
獲取模塊的路徑 fileName
const filename = Module._resolveFilename(request, parent, isMain);
-
根據 fileName 判斷是否存在該模塊的緩存
- 如果存在緩存,則調用
updateChildren
方法在更新緩存內容,並返回緩存 - 如果不存在緩存,則繼續執行
- 如果存在緩存,則調用
-
當做原生模塊,調用
loadNativeModule
方法進行加載- 如果加載成功,則返回該原生模塊
- 否則,繼續執行
-
根據當前模塊名(路徑)和父模塊對象生成一個 Module 實例:
const module = cachedModule || new Module(filename, parent);
- 再判斷該模塊是否是入口文件
if (isMain) {
process.mainModule = module;
module.id = '.';
}
- 將該模塊的實例存入到 Module 的緩存中
Module._cache[filename] = module;
- 該模塊的實例調用自身的
load
方法,根據 fileName 加載模塊
module.load(filename);
- 獲取該模塊文件的后綴名稱
const extension = findLongestRegisteredExtension(filename);
如果后綴名稱是ES Module格式的(.mjs),則判斷Module是否支持.mjs文件的解析,如果不支持,則拋出異常。
- 根據后綴名稱解析模塊文件內容
Module._extensions[extension](this, filename);
- 根據fileName讀取文件內容
content = fs.readFileSync(filename, 'utf8');
- 編譯並執行讀取到的文件,調用 module 自身的
_complile
方法:
module._compile(content, filename);
_compile
主要內容步驟:
const compiledWrapper = wrapSafe(filename, content, this);
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
const exports = this.exports;
const thisValue = exports;
const module = this;
result = compiledWrapper.call(thisValue, exports, require, module, filename, dirname);
return result;
wrapSafe
方法的返回值
具體獲得上圖結果的代碼是:
const wrapper = Module.wrap(content);
return vm.runInThisContext(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: async (specifier) => {
const loader = asyncESM.ESMLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
},
});
- 修改該模塊的加載狀態為true
this.loaded = true;
- 加載成功。
總結
通過上面的調試過程可得出以下結論:
- 在NodeJs中,從入口文件開始,一切皆 Module。
- 模塊的加載是同步的。
- 由於緩存機制的存在,模塊的循環引用對性能的影響微乎其微,並且循環引用到的模塊可能是不完整的,並且可能會導致錯
- require 查找模塊的流程如下:
- 文件路徑的解析流程圖如下:
本文完
學習有趣的知識,結識有趣的朋友,塑造有趣的靈魂!
大家好!我是〖編程三昧〗的作者 隱逸王,我的公眾號是『編程三昧』,歡迎關注,希望大家多多指教!
知識與技能並重,內力和外功兼修,理論和實踐兩手都要抓、兩手都要硬!