注: 1. 本文涉及的nodejs源碼如無特別說明則所有基於 v10.14.1
若是你對NodeJs系列感興趣,歡迎關注微信公衆號:前端神盾局或 github NodeJs系列文章html
本節主要基於NodeJs源碼,對其模塊的實現作一個簡要的概述,若有錯漏,望諸君不吝指正。前端
當咱們使用require
引入一個模塊的時候,概況起來經歷了兩個步驟:路徑分析和模塊載入node
路徑分析其實就是模塊查找的過程,由_resolveFilename函數實現。git
咱們經過一個例子,展開說明:github
const http = require('http'); const moduleA = requie('./parent/moduleA');
這個例子中,咱們引入兩種不一樣類型的模塊:核心模塊-http
和自定義模塊moduleA
json
對於核心模塊而言,_resolveFilename
會跳過查找步驟,直接返回,交給下一步處理api
if (NativeModule.nonInternalExists(request)) { // 這裏的request 就是模塊名稱 'http' return request; }
而對於自定義模塊而言,存在如下幾種狀況(_findPath)緩存
這些在官方文檔中已經闡述的很清楚了,這裏就再也不贅述。微信
若是模塊存在,那麼_resolveFilename
會返回該模塊的絕對路徑,好比/Users/xxx/Desktop/practice/node/module/parent/moduleA.js
。app
獲取到模塊地址後,Node就開始着手載入模塊。
首先,Node會查看模塊是否存在緩存中:
// filename 即模塊絕對路徑 var cachedModule = Module._cache[filename]; if (cachedModule) { updateChildren(parent, cachedModule, true); return cachedModule.exports; }
存在則返回對應緩存內容,不存在則進一步判斷該模塊是不是核心模塊:
if (NativeModule.nonInternalExists(filename)) { return NativeModule.require(filename); }
若是模塊既不存在於緩存中也非核心模塊,那麼Node會實例化一個全新的模塊對象
function Module(id, parent){ // 一般是模塊絕對路徑 this.id = id; // 要導出的內容 this.exports = {}; // 父級模塊 this.parent = parent; this.filename = null; // 是否已經加載成功 this.loaded = false; // 子模塊 this.children = []; } var module = new Module(filename, parent);
然後Node會根據路徑嘗試載入。
function tryModuleLoad(module, filename) { var threw = true; try { module.load(filename); threw = false; } finally { if (threw) { delete Module._cache[filename]; } } }
對於不一樣的文件擴展名,其載入方法也有所不一樣。
經過fs同步讀取文件內容後將其包裹在指定函數中:
Module.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
調用執行此函數:
compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
經過fs同步讀取文件內容後,用JSON.parse
解析並返回內容
var content = fs.readFileSync(filename, 'utf8'); try { module.exports = JSON.parse(stripBOM(content)); } catch (err) { err.message = filename + ': ' + err.message; throw err; }
這是用C/C++編寫的擴展文件,經過dlopen()方法加載最後編譯生成的文件。
return process.dlopen(module, path.toNamespacedPath(filename));
這是用於處理ES6模塊的擴展文件,是NodeJs在v8.5.0後新增的特性。對於這類擴展名的文件,只能使用ES6模塊語法import
引入,不然將會報錯(啓用 --experimental-modules
的狀況下)
throw new ERR_REQUIRE_ESM(filename);
若是一切順利,就會返回附加在exports對象上的內容
return module.exports;
接下來咱們來探究一下模塊循環依賴的問題:模塊1依賴模塊2,模塊2依賴模塊1,會發生什麼?
這裏只探究commonjs的狀況
爲此,咱們建立了兩個文件,module-a.js和module-b.js,並讓他們相互引用:
module-a.js
console.log(' 開始加載 A 模塊'); exports.a = 2; require('./module-b.js'); exports.b = 3; console.log('A 模塊加載完畢');
module-b.js
console.log(' 開始加載 B 模塊'); let moduleA = require('./module-a.js'); console.log(moduleA.a,moduleA.b) console.log('B 模塊加載完畢');
運行module-a.js
,能夠看到控制檯輸出:
開始加載 A 模塊 開始加載 B 模塊 2 undefined B 模塊加載完畢 A 模塊加載完畢
這時由於每一個require
都是同步執行的,在module-a
徹底加載前須要先加載./module-b
,此時對於module-a
而言,其exports
對象上只附加了屬性a
,屬性b
是在./module-b
加載完成後才賦值的。
能夠經過delete require.cache(moduleId)
來刪除對應模塊的緩存,其中moduleId表示的是模塊的絕對路徑,通常的,若是咱們須要對某些模塊進行熱更新,可使用此特性,舉個例子:
// hot-reload.js console.log('this is hot reload module'); // index.js const path = require('path'); const fs = require('fs'); const hotReloadId = path.join(__dirname,'./hot-reload.js'); const watcher = fs.watch(hotReloadId); watcher.on('change',(eventType,filename)=>{ if(eventType === 'change'){ delete require.cache[hotReloadId]; require(hotReloadId); } });
從8.5.0版本開始,NodeJs開始支持原生ES6模塊,啓用該功能須要兩個條件:
node --experimental-modules index.mjs
node --experimental-modules index.mjs
可是截止到NodeJs v10.15.0,ES6模塊的支持依舊是實驗性的,筆者並不推薦在公司項目中使用