1.CommonJS規範的原由html
1.JavaScript沒有模塊系統。沒有原生的支持密閉做用域或依賴管理。 2.JavaScript沒有標準庫。除了一些核心庫外,沒有文件系統的API,沒有IO流API等。 3.JavaScript沒有標準接口。沒有如Web Server或者數據庫的統一接口。 4.JavaScript沒有包管理系統。不能自動加載和安裝依賴。
2.CommonJS對模塊的定義十分簡單,主要分爲模塊引用、模塊定義和模塊標識3個部分。node
1.模塊引用 require('xxx') 2.模塊定義 exports.xxx module.exports = {} 3.模塊標識 模塊標識其實就是傳遞給require()方法的參數,它必須是符合小駝峯命名的字符串, 或者以.、..開頭的相對路徑,或者絕對路徑。它能夠沒有文件名後綴.js。
3.Nodejs模塊分類
Node.js的模塊分爲兩類,一類爲核心模塊(node提供),一類爲文件模塊(用戶編寫)。核心模塊在Node.js源代碼編譯的時候編譯進了二進制執行文件,在nodejs啓動過程當中,部分核心模塊直接加載進了內存中,因此這部分模塊引入時能夠省略文件定位和編譯執行兩個步驟,因此加載的速度最快。另外一類文件模塊是動態加載的,加載速度比核心模塊慢。可是Node.js對核心模塊和文件模塊都進行了緩存,因而在第二次require時,是不會有重複開銷的。其中原生模塊都被定義在lib這個目錄下面,文件模塊則不定性。
ps:核心模塊又分爲兩部分,C/C++編寫的和Javascript編寫的,前者在源碼的src目錄下,後者則在lib目錄下。(lib/*.js)(其中lib/internal部分不提供給文件模塊)c++
注:經過process.moduleLoadList能夠查看已經加載的核心模塊。 核心模塊 = 原生模塊git
4.模塊引入三步走github
1.路徑分析 2.文件定位 3.編譯執行
1.路徑分析
核心模塊:如http、fs、path等,速度僅次於緩存。
路徑形式的文件:以.或者..開始的相對路徑,以/開始的絕對路徑。
自定義模塊:不屬於核心模塊也不屬於路徑形式的標識符。它是一種特殊的文件模塊,多是一個文件或者包的形式。這類模塊的查找是最費時的,也是全部方式中最慢的一種。在定位時,會給出一個可能路徑的數組
沒有mac電腦,直接copy阮老師的圖 http://www.ruanyifeng.com/blo...
數據庫
關鍵源碼
https://github.com/nodejs/nod...json
lib/module.js開頭就使用了require,略矇蔽?不要着急,這個js文件中的reuiqre與咱們日常使用的是不同的,此處的require實際上是NativeModule.require。而NativeModule的定義在https://github.com/nodejs/nod...
node在啓動的時候會去執行bootstrap_node.js這個模塊,後續會對此進行分析,暫時只需明白module.js中的require與咱們文件模塊中使用的require不是同一個便可bootstrap
路徑分析代碼追蹤棧
Module.prototype.require --> Module._load --> Module._resolveFilename --> Module._resolveLookupPaths --> Module._findPath(文件定位) --> fileName(文件絕對路徑)
幾個方法的做用小結:
Module.prototype.require:直接調用Module._load並return
Module._load:調用Module._resolveFilename獲取文件絕對路徑,而且根據該絕對路徑添加緩存以及編譯模塊
Module._resolveFilename:獲取文件絕對路徑
Module._resolveLookupPaths:獲取文件可能路徑
Module._findPath:根據文件可能路徑定位文件絕對路徑,包括後綴補全(.js, .json, .node)等都在此方法中執行,最終返回文件絕對路徑數組
Module.prototype.require = function(path) { assert(path, 'missing path'); assert(typeof path === 'string', 'path must be a string'); return Module._load(path, this, /* isMain */ false); };
Module._load = function(request, parent, isMain) { if (parent) { debug('Module._load REQUEST %s parent: %s', request, parent.id); } var filename = Module._resolveFilename(request, parent, isMain); // 檢測緩存 var cachedModule = Module._cache[filename]; if (cachedModule) { updateChildren(parent, cachedModule, true); return cachedModule.exports; } // 檢測是不是核心模塊 if (NativeModule.nonInternalExists(filename)) { debug('load native module %s', request); return NativeModule.require(filename); } // Don't call updateChildren(), Module constructor already does. var module = new Module(filename, parent); // 判斷是不是入口模塊 if (isMain) { process.mainModule = module; module.id = '.'; } // 添加緩存(小碎步,因爲緩存在模塊編譯前就進行了設置,解決了循環依賴的問題!!!) Module._cache[filename] = module; // 編譯模塊 tryModuleLoad(module, filename); return module.exports; };
Module._resolveFilename = function(request, parent, isMain) { // 判斷是不是核心模塊 if (NativeModule.nonInternalExists(request)) { return request; } // 計算全部可能的路徑,對於核心模塊,相對路徑,絕對路徑,自定義模塊返回不一樣的數組 var paths; paths = Module._resolveLookupPaths(request, parent, true); // look up the filename first, since that's the cache key. // 計算文件的絕對路徑 var filename = Module._findPath(request, paths, isMain); if (!filename) { var err = new Error(`Cannot find module '${request}'`); err.code = 'MODULE_NOT_FOUND'; throw err; } return filename; }; // 暴露給文件模塊的文件定位方法 require.resolve = function(request) { return Module._resolveFilename(request, self); }; // 用法 require.resolve('a.js') // 返回 /home/ruanyf/tmp/a.js
Module._resolveLookupPaths代碼相對複雜,這裏簡單起見只展現一些其執行結果 tt.js文件目錄d/wedoctor node tt.js console.log(module.constructor._resolveLookupPaths('fs', module, true)) console.log(module.constructor._resolveLookupPaths('/hello', module, true)) console.log(module.constructor._resolveLookupPaths('../../hello', module, true)) console.log(module.constructor._resolveLookupPaths('hello', module, true)) 1.加載核心模塊的時候,返回 null 2.加載絕對路徑的時候,返回 [ 'D:\\wedoctor\\node_modules', 'D:\\node_modules', 'C:\\Users\\小韓\\.node_modules', 'C:\\Users\\小韓\\.node_libraries', 'D:\\tools\\nodejs\\lib\\node' ] 因爲是絕對路徑,因此在_findPath方法中會被清空 3.加載相對路徑的時候,返回 [ 'D:\\wedoctor' ] 4.加載自定義模塊的時候,返回 [ 'D:\\wedoctor\\node_modules', 'D:\\node_modules', 'C:\\Users\\小韓\\.node_modules', 'C:\\Users\\小韓\\.node_libraries', 'D:\\tools\\nodejs\\lib\\node' ] 上面的數組,就是模塊全部可能的路徑。基本上是,從當前路徑開始一級級向上尋找 node_modules 子目錄。 最後那三個路徑,主要是爲了歷史緣由保持兼容,實際上已經不多用了。
Module._findPath = function(request, paths) { // 列出全部可能的後綴名:.js,.json, .node var exts = Object.keys(Module._extensions); // 若是是絕對路徑,就再也不搜索 if (request.charAt(0) === '/') { paths = ['']; } // 是否有後綴的目錄斜槓 var trailingSlash = (request.slice(-1) === '/'); // 第一步:若是當前路徑已在緩存中,就直接返回緩存 var cacheKey = JSON.stringify({request: request, paths: paths}); if (Module._pathCache[cacheKey]) { return Module._pathCache[cacheKey]; } // 第二步:依次遍歷全部路徑 for (var i = 0, PL = paths.length; i < PL; i++) { var basePath = path.resolve(paths[i], request); var filename; if (!trailingSlash) { // 第三步:是否存在該模塊文件 filename = tryFile(basePath); if (!filename && !trailingSlash) { // 第四步:該模塊文件加上後綴名,是否存在 filename = tryExtensions(basePath, exts); } } // 第五步:目錄中是否存在 package.json if (!filename) { filename = tryPackage(basePath, exts); } if (!filename) { // 第六步:是否存在目錄名 + index + 後綴名 filename = tryExtensions(path.resolve(basePath, 'index'), exts); } // 第七步:將找到的文件路徑存入返回緩存,而後返回 if (filename) { Module._pathCache[cacheKey] = filename; return filename; } } // 第八步:沒有找到文件,返回false return false; };
至此在看_load方法中,以及獲取到的文件的絕對地址fileName,以此判斷緩存,是不是核心模塊,若是二者都不是,則進行模塊編譯tryModuleLoad。繼續看tryModuleLoad方法緩存
function tryModuleLoad(module, filename) { var threw = true; // 調用了module.load方法 try { module.load(filename); threw = false; } finally { if (threw) { delete Module._cache[filename]; } } }
Module.prototype.load = function(filename) { debug('load %j for module %j', filename, this.id); assert(!this.loaded); this.filename = filename; this.paths = Module._nodeModulePaths(path.dirname(filename)); var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; // 根據不一樣的文件後綴名,調用不一樣的方法 Module._extensions[extension](this, filename); this.loaded = true; };
// Native extension for .js Module._extensions['.js'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); module._compile(internalModule.stripBOM(content), filename); }; // Native extension for .json Module._extensions['.json'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); try { module.exports = JSON.parse(internalModule.stripBOM(content)); } catch (err) { err.message = filename + ': ' + err.message; throw err; } };
以js爲例最終調用module._compile方法
Module.prototype._compile = function (content, filename) { content = internalModule.stripShebang(content); // 添加函數包裹 var wrapper = Module.wrap(content); // 把字符串轉換成可用函數 var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true }); // 經過call方法,使模塊內部this指向module.exports對象,同時將一些方法做爲參數傳入模塊中 result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname); return result; }; Module.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
接下來還須要分析,NativeModule是如何注入到module.js中的,NativeModule又是如何加載核心模塊的?
NativeModule代碼在源碼目錄lib/internal/bootstrap_node.js中
bootstrap_node.js在node啓動中被調用,而且注入c++對象process,bootstrap_node.js 中會對process 進行部分信息初始化,其實這只是不多的一部分,大部分都在 c++ 部分初始化的。
在node.cc文件中咱們能夠發現process對象初始化代碼
以前咱們講到,nodejs源碼中Javascript編寫的核心模塊都會經過V8附帶的js2c.py工具轉換成C++裏面的數組,生成node_natives.h頭文件。
當調用process.binding('natives')時候,該方法將經過js2c.py工具轉換出的字符串數組取出,而後從新轉換爲普通字符串,以對Javascript核心模塊進行編譯和執行。
繼續在該文件中咱們能夠看到
在lib/module.js也是實現被v8工具轉換成c++數組,因此經過NativeModule.require('module')對module模塊進行編譯執行,而且獲取module模塊導出的Module對象,在執行該對象的runMain函數,回過頭來咱們看module.js
原來當咱們在命令行執行 node app.js時候 app.js就是process.argv[1],而Module._load這個方法以前就介紹過了。回頭來,在Node真正執行app.js以前,作了許多前置工做,包括process對象注入,核心模塊的加載等。
參考:
https://github.com/nodejs/nod...
https://github.com/nodejs/nod...
http://www.ruanyifeng.com/blo...
http://f2e.souche.com/blog/a-...