require() 方法詳解


在 NodeJS 中有一個方法是我們使用頻率最高的,那就是 require 方法。NodeJs 遵循 CommonJS 規范,該規范的核心是通過 require來加載其他依賴的模塊。

幾個問題

  1. module.exports 或者 exports 是全局變量嗎?
  2. 模塊的加載是同步還是異步?
  3. 循環引用會不會產生性能問題或者導致錯誤?

什么是 CommonJS

每一個文件就是一個模塊,擁有自己獨立的作用域,變量,以及方法等,對其他的模塊都不可見。CommonJS 規范規定,每個模塊內部,module 變量代表當前模塊。這個變量是一個對象,它的 exports 屬性(即module.exports)是對外的接口。

Node 模塊的分類

  1. build-in modules —— Nodejs 中以 C++ 形式提供的模塊。
  2. constant module —— Nodejs 中定義常量的模塊。
  3. native module —— Nodejs 中以 javascript 形式提供的模塊。
  4. 第三方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 實例,包括入口文件。

完整步驟:

  1. 調用父模塊的 require 方法(父模塊是指調用模塊的當前模塊)
require = function require(path) {
    return mod.require(path);
};
  1. 調用 Module 的 _load 方法

  2. 通過 Module._resolveFilename 獲取模塊的路徑 fileName

const filename = Module._resolveFilename(request, parent, isMain);
  1. 根據 fileName 判斷是否存在該模塊的緩存

    • 如果存在緩存,則調用 updateChildren 方法在更新緩存內容,並返回緩存
    • 如果不存在緩存,則繼續執行
  2. 當做原生模塊,調用 loadNativeModule 方法進行加載

    • 如果加載成功,則返回該原生模塊
    • 否則,繼續執行
  3. 根據當前模塊名(路徑)和父模塊對象生成一個 Module 實例:

const module = cachedModule || new Module(filename, parent);
  1. 再判斷該模塊是否是入口文件
if (isMain) {
    process.mainModule = module;
    module.id = '.';
}
  1. 將該模塊的實例存入到 Module 的緩存中
Module._cache[filename] = module;

image

  1. 該模塊的實例調用自身的 load 方法,根據 fileName 加載模塊
module.load(filename);
  1. 獲取該模塊文件的后綴名稱
const extension = findLongestRegisteredExtension(filename);

如果后綴名稱是ES Module格式的(.mjs),則判斷Module是否支持.mjs文件的解析,如果不支持,則拋出異常。

  1. 根據后綴名稱解析模塊文件內容
Module._extensions[extension](this, filename);
  1. 根據fileName讀取文件內容
content = fs.readFileSync(filename, 'utf8');
  1. 編譯並執行讀取到的文件,調用 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方法的返回值

image

具體獲得上圖結果的代碼是:

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));
    },
});
  1. 修改該模塊的加載狀態為true
this.loaded = true;
  1. 加載成功。

總結

通過上面的調試過程可得出以下結論:

  1. 在NodeJs中,從入口文件開始,一切皆 Module
  2. 模塊的加載是同步的。
  3. 由於緩存機制的存在,模塊的循環引用對性能的影響微乎其微,並且循環引用到的模塊可能是不完整的,並且可能會導致錯
  4. require 查找模塊的流程如下:

image

  1. 文件路徑的解析流程圖如下:

image

本文完

學習有趣的知識,結識有趣的朋友,塑造有趣的靈魂!

大家好!我是〖編程三昧〗的作者 隱逸王,我的公眾號是『編程三昧』,歡迎關注,希望大家多多指教!

知識與技能並重,內力和外功兼修,理論和實踐兩手都要抓、兩手都要硬!


免責聲明!

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



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