【 js 模塊加載 】【源碼學習】深入學習模塊化加載(node.js 模塊源碼)


文章提綱:

  第一部分:介紹模塊規范及之間區別

  第二部分:以 node.js 實現模塊化規范 源碼,深入學習。

 

一、模塊規范

說到模塊化加載,就不得先說一說模塊規范。模塊規范是用來約束每個模塊,讓其必須按照一定的格式編寫。AMD,CMD,CommonJS 是目前最常用的三種模塊化書寫規范。 

1、AMD(Asynchronous Module Definition):異步模塊定義,所謂異步是指模塊和模塊的依賴可以被異步加載,他們的加載不會影響它后面語句的運行。有效避免了采用同步加載方式中導致的頁面假死現象。AMD代表:RequireJS。
 
它主要有兩個接口:define 和 require。define 是模塊開發者關注的方法,而 require 則是模塊使用者關注的方法。 
      1.1、define() 函數:

define(id?, dependencies?, factory);
//id :可選參數,它指的是模塊的名字。
//dependencies:可選參數,定義中模塊所依賴模塊的數組。
//factory:模塊初始化要執行的函數或對象
需要注意的是,dependencies有多少個元素,factory就有多少個傳參,位置一一對應。
使用栗子:
1 define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {  
2     exports.verb = function() {            
3         return beta.verb();            
4         //Or:
5         //return require("beta").verb();        
6     }    
7 });

 

      1.2、require() 函數

require([module], callback);
//module:一個數組,里面的成員就是要加載的模塊.
//callback:模塊加載成功之后的回調函數。

需要注意的是 ,module 有多少個元素,callback 就有多少個傳參,位置一一對應。

使用的栗子:

require(["a","b","c"],function(a,b,c){
    //code here
});

具體的使用詳細,大家可以去官網學習: https://github.com/amdjs/amdjs-api/wiki/AMD-(%E4%B8%AD%E6%96%87%E7%89%88)
 
2、CMD(Common Module Definition):通用模塊定義,本質上也是異步的加載模塊,采用的是懶加載方式即按需加載。CMD代表:SeaJS。
 
它主要有一個接口:define 是模塊開發者關注的方法。
define(factory);
//factory:模塊初始化要執行的函數或對象,為函數時,表示是模塊的構造方法。
//執行該構造方法,可以得到模塊向外提供的接口。
//factory 方法在執行時,默認會傳入三個參數:require、exports 和 module。
//其中require用來獲取其他模塊提供的接口,exports用來向外提供模塊接口,module是一個對象,上面存儲了與當前模塊相關聯的一些屬性和方法。

使用的栗子: 

 1 define(function(require, exports, module) {
 2     var a = require('./a')
 3     a.doSomething()
 4     // 此處略去 100 行
 5     var b = require('./b') // 依賴可以就近書寫
 6     b.doSomething()
 7     //
 8     // 對外提供 doSomething 方法
 9     exports.doSomething = function() {};
10 });

而調用CMD編寫的模塊的方法是:

1 seajs.use("a")//調用a模塊
2 //這里就設計到SeaJS的使用了:
3 //- 引入sea.js的庫
4 //- 如何變成模塊?
5 //      - define
6 //- 如何調用模塊?
7 //      -sea.js.use
8 //- 如何依賴模塊?
9 //      -require

具體的使用詳細,建議大家可以去官網學習: https://github.com/seajs/seajs/issues/242
 
 
3、CommonJS :采用同步加載模塊的方式,也就是說只有加載完成,才能執行后面的操作。CommonJS 代表:Node 應用中的模塊,通俗的說就是你用 npm 安裝的模塊。
它使用 require 引用和加載模塊,exports 定義和導出模塊,module 標識模塊。使用 require 時需要去讀取並執行該文件,然后返回 exports 導出的內容。
 1  //定義模塊 math.js
 2  var random=Math.random()*10;
 3  function printRandom(){
 4      console.log(random)
 5  }
 6 
 7  function printIntRandom(){
 8      console.log(Math.floor(random))
 9  }
10  //模塊輸出
11  module.exports={
12      printRandom:printRandom,
13      printIntRandom:printIntRandom
14  }
15  //加載模塊 math.js
16  var math=require('math')
17  //調用模塊提供的方法
18  math.printIntRandom()
19  math.printRandom()

 

4、模塊規范之間的區別

     A、首先說一下 CommonJS與其它兩種的區別:CommonJS采用的就是同步加載方式,而其它兩種都是異步的。

   舉個栗子:

commonJS中:

1 var math = require('math');
2 math.add(2, 3);

第二行 math.add(2, 3),在第一行 require('math') 之后運行,因此必須等 math.js 加載完成。也就是說,如果加載時間很長,整個應用就會停在那里等。

AMD中:

1 require(['math'], function (math) {
2   math.add(2, 3);
3 });
4 
5 console.log("222");

這個是不會阻遏后面語句的執行的,等到什么時候 math 模塊加載出來進行回調函數就可以了。

PS:由於 Node.js 主要用於服務器編程,模塊文件一般都已經存在於本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以 CommonJS 規范比較適用。但是,如果是瀏覽器環境,要從服務器端加載模塊,這時就必須采用非同步模式,因此瀏覽器端一般采用 AMD 規范。 
 
     B、再說一下 AMD 和 CMD 的區別:
最主要的區別就是執行模塊的機制大不一樣:
SeaJS(CMD) 對模塊的態度是懶執行, 而 RequireJS(AMD) 對模塊的態度是預執行。
SeaJS 只會在真正需要使用(依賴)模塊時才執行該模塊。執行模塊的順序也是嚴格按照模塊在代碼中出現(require)的順序。
而 RequireJS 會先盡早地執行(依賴)模塊, 相當於所有的 require 都被提前了, 而且模塊執行的順序也不一定100%按照順序。 
  
如果大家沒有明白,可以參考文章: https://www.douban.com/note/283566440/  有栗子,更形象。 
 
二、深入學習模塊化加載
 
下面從源碼深入了解一下所謂的模塊加載系統到底是如何運作的。
因為現在工作中用的大部分模塊都是 node_modules 也就是 CommonJS 模塊規范,所以我就以 node.js 實現模塊的源碼來分析:
 
前提知識:
      知識點一:主入口文件 即主模塊。在 require 方法中引用的 Module._load(path,parent,isMain),第三個參數 isMain 表示是不是主入口文件。對於 foo.js 文件,如果通過 node foo.js 運行則為 true,但如果通過 require('./foo') 運行則為 false。
     知識點二:涉及到的模塊類型:
     1、核心模塊:指的 lib 目錄下排除 lib/internal 文件下的模塊。是那些被編譯進 Node 的二進制模塊,它們被預置在 Node 中,提供 Node 的基本功能,如fs、http、https等。核心模塊使用 C/C++ 實現,外部使用 JS 封裝。要加載核心模塊,直接在代碼文件中使用 require() 方法即可,參數為模塊名稱,Node 將自動從核心模塊文件夾中進行加載。注意加載核心模塊只能用模塊名。核心模塊擁有最高的加載優先級,即使已經有了一個同名的第三方模塊,核心模塊也會被優先加載。
     2、內部模塊:指的是 lib/internal 文件夾下的模塊,這些模塊僅僅供 Node.js 核心的內部使用,不能被外部使用。
 

  

通常我們在使用一個模塊的時候在 js 中都是這樣引用的:

var math = require('math');
math.add(2, 3);

從 require 方法本身是如何實現的入手,一步一步看:(代碼全部來自 node.js [https://github.com/nodejs/node] 源碼)

 

require 方法封裝在 node 源碼中的 lib 文件夾里的 module.js 中

 1 // Loads a module at the given file path. Returns that module's
 2 // `exports` property.
 3 // 給定一個模塊目錄,返回該模塊的 exports 屬性
 4 Module.prototype.require = function(path) {
 5   // assert() 頭部引入,主要用於斷言,如果表達式不符合預期,就拋出一個錯誤。
 6   // assert方法接受兩個參數,當第一個參數對應的布爾值為true時,不會有任何提示,返回undefined。
 7   // 當第一個參數對應的布爾值為false時,會拋出一個錯誤,該錯誤的提示信息就是第二個參數設定的字符串。
 8   assert(path, 'missing path');  //斷言是否有path
 9   assert(typeof path === 'string', 'path must be a string'); //斷言 path是否是個字符串
10 
11   return Module._load(path, this, /* isMain */ false);  //require方法主要是為了引出_load方法。
12   //_load函數三個參數: path 當前加載的模塊名稱,parent 父親模塊,其實是誰導入了該模塊,
13   // /* isMain */ false  是不是主入口文件
14 };

require 中調用了 Module._load()方法:
 1 // Check the cache for the requested file.
 2 // 1. If a module already exists in the cache: return its exports object.
 3 // 2. If the module is native: call `NativeModule.require()` with the
 4 //    filename and return the result.
 5 // 3. Otherwise, create a new module for the file and save it to the cache.
 6 //    Then have it load  the file contents before returning its exports
 7 //    object.
 8 // 從緩存中查找所要加載的模塊
 9 // 1. 如果一個模塊已經存在於緩存中:直接返回它的exports對象
10 // 2. 如果模塊是一個本地模塊,調用'NativeModule.require()'方法,filename作為參數,並返回結果
11 // 3. 否則,使用這個文件創建一個新模塊並把它加入緩存中。在加載它只會返回exports對象。
12 // _load函數三個參數: path 當前加載的模塊名稱,parent 父親模塊,/* isMain */ false  是不是主入口文件
13 Module._load = function(request, parent, isMain) { 
14   if (parent) {
15       //頭部引入了 Module._debug = util.debuglog('module');const debug = Module._debug;
16     // 這個方法用來打印出調試信息,具體可以看 https://chyingp.gitbooks.io/nodejs/%E6%A8%A1%E5%9D%97/util.html
17     debug('Module._load REQUEST %s parent: %s', request, parent.id); 
18 
19   }
20 
21   // 找到當前的需要解析的文件名
22   var filename = Module._resolveFilename(request, parent, isMain);
23 
24   //如果已經有的緩存,直接返回緩存的exports
25   var cachedModule = Module._cache[filename];
26   if (cachedModule) {
27     return cachedModule.exports;
28   }
29 
30   //如果模塊是一個內部模塊,調用內部方法'NativeModule.require()'方法,filename作為參數,並返回結果
31   if (NativeModule.nonInternalExists(filename)) {
32     debug('load native module %s', request);
33     return NativeModule.require(filename);
34   }
35 
36   //創建一個新模塊
37   var module = new Module(filename, parent);
38 
39   //是否為主模塊,
40   if (isMain) {
41     //主模塊的話,需要將當前的module賦值給process.mainModule
42     process.mainModule = module;
43     //主模塊的id特殊的賦值為"."
44     module.id = '.';
45   }
46 
47   //並把新模塊加入緩存中
48   Module._cache[filename] = module;
49 
50   //嘗試導入模塊的操作
51  tryModuleLoad(module, filename);
52 
53   // 返回新創建模塊中的exports,也就是暴露在外面的方法屬性等。
54   return module.exports;
55 };

Module._load 中調用了 Module._resolveFilename() 方法

 1 // 負責具體filename的文件查找
 2 // 參數 request 當前加載的模塊名稱,parent 父親模塊,/* isMain */ false  是不是主入口文件
 3 Module._resolveFilename = function(request, parent, isMain) { 
 4 
 5   //NativeModule用於管理js模塊,頭部引入的。
 6   //NativeModule.nonInternalExists()用來判斷是否是原生模塊且不是內部模塊,
 7   //所謂內部模塊就是指 lib/internal 文件目錄下的模塊,像fs等。
 8   //滿足 是原生模塊且不是內部模塊,則直接返回 當前加載的模塊名稱request。
 9   if (NativeModule.nonInternalExists(request)) {
10     return request;
11   }
12 
13   // Module._resolveLookupPaths()函數返回一個數組[id , paths], 
14   // paths是一個 可能 包含這個模塊的文件夾路徑(絕對路徑)數組
15   var paths = Module._resolveLookupPaths(request, parent, true);
16 
17   // look up the filename first, since that's the cache key.
18   // 確定哪一個路徑為真,並且添加到緩存中
19   var filename = Module._findPath(request, paths, isMain);
20 
21   // 如果沒有找到模塊,報錯
22   if (!filename) {
23     var err = new Error(`Cannot find module '${request}'`);
24     err.code = 'MODULE_NOT_FOUND';
25     throw err;
26   }
27 
28   // 找到模塊則直接返回
29   return filename;
30 };

Module._resolveFilename 調用了 Module._resolveLookupPaths() 方法 和 Module._findPath() 方法。

這兩個方法主要是對模塊路徑的查找,這里要說一下 node 模塊路徑解析,方便對下面兩個函數的理解,大家可以對照着理解。

根據require函數的參數形式的不同,比如說直接引一個文件名 require("moduleA"),或者是路徑require("./moduleA")等,查找方式會有一些變化:

從 Y 路徑的模塊 require(X) 
1. 如果 X 是一個核心模塊,    
    a. 返回核心模塊 //核心模塊是指node.js下lib的內容    
    b. 結束 
2. 如果 X 是以 './' 或 '/' 或 '../' 開頭    
    a. 加載文件(Y + X)    
    b. 加載目錄(Y + X) 
3. 加載Node模塊(X, dirname(Y)) // 導入一個NODE_MODULE,返回 
4. 拋出 "未找到" // 上述都沒找到,直接排出沒找到的異常。 
 加載文件(X) 
1. 如果 X 是一個文件,加載 X 作為 JavaScript 文本。結束 
2. 如果 X.js 是一個文件,加載 X.js 作為 JavaScript 文本。結束 
3. 如果 X.json 是一個文件,解析 X.json 成一個 JavaScript 對象。結束 
4. 如果 X.node 是一個文件,加載 X.node 作為二進制插件。結束 

加載目錄(X) 
1. 如果 X/package.json 是一個文件,    
    a. 解析 X/package.json,查找 "main" 字段    
    b. let M = X + (json main 字段)    
    c. 加載文件(M) 
2. 如果 X/index.js 是一個文件,加載  X/index.js 作為 JavaScript 文本。結束 
3. 如果 X/index.json 是一個文件,解析 X/index.json 成一個 JavaScript 對象。結束 
4. 如果 X/index.node 是一個文件,加載  X/index.node 作為二進制插件。結束 

加載Node模塊(X, START) 
1. let DIRS=NODE_MODULES_PATHS(START) //得到 node_module 文件目錄 
2. for each DIR in DIRS: // 遍歷所有的路徑 直到找到 x ,x 可能是 文件或者是目錄    
    a. 加載文件(DIR/X)    
    b. 加載目錄(DIR/X) 

NODE_MODULES_PATHS(START) //具體NODE_MODULES文件目錄算法 
1. let PARTS = path split(START) 
2. let I = count of PARTS - 1 
3. let DIRS = [] 
4. while I >= 0,    
    a. if PARTS[I] = "node_modules" CONTINUE    
    b. DIR = path join(PARTS[0 .. I] + "node_modules")    
    c. DIRS = DIRS + DIR    
    d. let I = I - 1 5. return DIRS

 

1、Module._resolveLookupPaths() 方法

  1 // 'index.' character codes
  2 var indexChars = [ 105, 110, 100, 101, 120, 46 ];
  3 var indexLen = indexChars.length;
  4 //_resolveLookupPaths() 方法用來查找模塊,返回一個數組,數組第一項為模塊名稱即request,數組第二項返回一個可能包含這個模塊的文件夾路徑數組
  5 //
  6 //處理了如下幾種情況:
  7 // 1、是原生模塊且不是內部模塊
  8 // 2、如果路徑不以"./" 或者'..'開頭或者只有一個字符串,即是引用模塊名的方式,即require('moduleA');
  9 //   2.1以 '/' 為前綴的模塊是文件的絕對路徑。 例如,require('/home/marco/foo.js') 會加載 /home/marco/foo.js 文件。
 10 //   2.2以 './' 為前綴的模塊是相對於調用 require() 的文件的。 也就是說,circle.js 必須和 foo.js 在同一目錄下以便於 require('./circle') 找到它。
 11 //   2.3當沒有以 '/'、'./' 或 '../' 開頭來表示文件時,這個模塊必須是一個核心模塊或加載自 node_modules 目錄。
 12 Module._resolveLookupPaths = function(request, parent, newReturn) { //request 當前加載的模塊名稱,parent 父親模塊
 13 
 14   //NativeModule用於管理js模塊,頭部引入的。
 15   //NativeModule.nonInternalExists()用來判斷是否是原生模塊且不是內部模塊,所謂內部模塊就是指 lib/internal 文件目錄下的模塊,像fs等。
 16   if (NativeModule.nonInternalExists(request)) {
 17     debug('looking for %j in []', request);
 18 
 19     //滿足 是原生模塊且不是內部模塊,也就是說是node.js下lib文件夾下的模塊,
 20     //但不包含lib/internal 文件目錄下的模塊,並且newReturn 為true,則返回null ,
 21     //如果newReturn 為false 則返回[request, []]。
 22     return (newReturn ? null : [request, []]);
 23   }
 24 
 25   // Check for relative path
 26   // 檢查相關路徑
 27   // 如果路徑不以"./"或者'..'開頭或者只有一個字符串,即是引用模塊名的方式,即require('moduleA');
 28   if (request.length < 2 ||
 29       request.charCodeAt(0) !== 46/*.*/ ||
 30       (request.charCodeAt(1) !== 46/*.*/ &&
 31        request.charCodeAt(1) !== 47/*/*/)) {
 32       //全局變量,在Module._initPaths 函數中賦值的變量,modulePaths記錄了全局加載依賴的根目錄
 33     var paths = modulePaths; 
 34 
 35     // 設置一下父親的路徑,其實就是誰導入了當前模塊
 36     if (parent) {
 37       if (!parent.paths)
 38         paths = parent.paths = [];
 39       else
 40         paths = parent.paths.concat(paths);
 41     }
 42 
 43     // Maintain backwards compat with certain broken uses of require('.')
 44     // by putting the module's directory in front of the lookup paths.
 45     // 如果只有一個字符串,且是 .
 46     if (request === '.') {
 47       if (parent && parent.filename) {
 48         paths.unshift(path.dirname(parent.filename));
 49       } else {
 50         paths.unshift(path.resolve(request));
 51       }
 52     }
 53 
 54     debug('looking for %j in %j', request, paths);
 55 
 56     //直接返回
 57     return (newReturn ? (paths.length > 0 ? paths : null) : [request, paths]);
 58   }
 59 
 60   // with --eval, parent.id is not set and parent.filename is null
 61   // 處理父親模塊為空的情況
 62   if (!parent || !parent.id || !parent.filename) {
 63     // make require('./path/to/foo') work - normally the path is taken
 64     // from realpath(__filename) but with eval there is no filename
 65     // 生成新的目錄, 在系統目錄 modulePaths,當前目錄 和 "node_modules" 作為候選的路徑
 66     var mainPaths = ['.'].concat(Module._nodeModulePaths('.'), modulePaths);
 67 
 68     debug('looking for %j in %j', request, mainPaths);
 69     //直接返回
 70     return (newReturn ? mainPaths : [request, mainPaths]);
 71   }
 72 
 73   // Is the parent an index module?
 74   // We can assume the parent has a valid extension,
 75   // as it already has been accepted as a module.
 76   // 處理父親模塊是否為index模塊,即 path/index.js 或者 X/index.json等 帶有index字樣的module
 77   const base = path.basename(parent.filename); // path.basename()返回路徑中的最后一部分
 78   var parentIdPath;
 79   if (base.length > indexLen) {
 80     var i = 0;
 81 
 82     //檢查 引入的模塊名中是否有 "index." 字段,如果有, i === indexLen。
 83     for (; i < indexLen; ++i) {
 84       if (indexChars[i] !== base.charCodeAt(i))
 85         break;
 86     }
 87 
 88     // 匹配 "index." 成功,查看是否有多余字段以及剩余部分的匹配情況
 89     if (i === indexLen) {
 90       // We matched 'index.', let's validate the rest
 91       for (; i < base.length; ++i) {
 92         const code = base.charCodeAt(i);
 93 
 94         // 如果模塊名中有  除了 _, 0-9,A-Z,a-z 的字符 則跳出,繼續下一次循環
 95         if (code !== 95/*_*/ &&
 96             (code < 48/*0*/ || code > 57/*9*/) &&
 97             (code < 65/*A*/ || code > 90/*Z*/) &&
 98             (code < 97/*a*/ || code > 122/*z*/))
 99           break;
100       }
101 
102 
103       if (i === base.length) {
104         // Is an index module
105         parentIdPath = parent.id;
106       } else {
107         // Not an index module
108         parentIdPath = path.dirname(parent.id); //path.dirname() 返回路徑中代表文件夾的部分
109       }
110     } else {
111       // Not an index module
112       parentIdPath = path.dirname(parent.id);
113     }
114   } else {
115     // Not an index module
116     parentIdPath = path.dirname(parent.id);
117   }
118 
119   //拼出絕對路徑
120   //path.resolve([from ...], to) 將 to 參數解析為絕對路徑。
121   //eg:path.resolve('/foo/bar', './baz') 輸出'/foo/bar/baz'
122   var id = path.resolve(parentIdPath, request);  
123 
124   // make sure require('./path') and require('path') get distinct ids, even
125   // when called from the toplevel js file
126   // 確保require('./path')和require('path')兩種形式的,獲得不同的 ids
127   if (parentIdPath === '.' && id.indexOf('/') === -1) {
128     id = './' + id;
129   }
130 
131   debug('RELATIVE: requested: %s set ID to: %s from %s', request, id,
132         parent.id);
133   //path.dirname() 返回路徑中代表文件夾的部分
134   var parentDir = [path.dirname(parent.filename)]; 
135 
136   debug('looking for %j in %j', id, parentDir);
137 
138   // 當我們以"./" 等方式require時,都是以當前引用他的模塊,也就是父親模塊為對象路徑的
139   return (newReturn ? parentDir : [id, parentDir]);
140 };

 

2、Module._findPath() 方法

  1 var warned = false;
  2 //_findPath用於從可能的路徑中確定哪一個路徑為真,並且添加到緩存中
  3 //參數request 當前加載的模塊名稱,
  4 //paths ,Module._resolveLookupPaths()函數返回一個數組[id , paths],即模塊可能在的所有路徑,
  5 // /* isMain */ false  是不是主入口文件
  6 Module._findPath = function(request, paths, isMain) {
  7 
  8   //path.isAbsolute()判斷參數 path 是否是絕對路徑。
  9   if (path.isAbsolute(request)) {  
 10     paths = [''];
 11   } else if (!paths || paths.length === 0) {
 12     return false;
 13   }
 14 
 15 
 16   var cacheKey = request + '\x00' +
 17                 (paths.length === 1 ? paths[0] : paths.join('\x00'));
 18   var entry = Module._pathCache[cacheKey];
 19 
 20   //判斷是否在緩存中,如果有則直接返回
 21   if (entry)
 22     return entry;
 23 
 24   //如果不在緩存中,則開始查找
 25   var exts;
 26   // 當前加載的模塊名稱大於0位並且最后一位是 / ,即是否有后綴的目錄斜杠
 27   var trailingSlash = request.length > 0 &&
 28                       request.charCodeAt(request.length - 1) === 47/*/*/;
 29 
 30   // For each path
 31   // 循環每一個可能的路徑paths
 32   for (var i = 0; i < paths.length; i++) {
 33 
 34     // Don't search further if path doesn't exist
 35     // 如果路徑存在就繼續執行,不存在就繼續檢驗下一個路徑 stat 獲取路徑狀態
 36     const curPath = paths[i];
 37     if (curPath && stat(curPath) < 1) continue;
 38     var basePath = path.resolve(curPath, request); //生成絕對路徑
 39     var filename;
 40 
 41     //stat 頭部定義的函數,用來獲取路徑狀態,判斷路徑類型,是文件還是文件夾
 42     var rc = stat(basePath);
 43     //如果沒有后綴的目錄斜杠,那么就有可能是文件或者是文件夾名
 44     if (!trailingSlash) {
 45       // 若是文件
 46       if (rc === 0) {  // File.
 47 
 48         // 如果是使用模塊的符號路徑而不是真實路徑,並且不是主入口文件
 49         if (preserveSymlinks && !isMain) {  
 50           filename = path.resolve(basePath);
 51         } else {
 52           filename = toRealPath(basePath); //獲取當前執行文件的真實路徑
 53         }
 54 
 55       // 若是目錄
 56       } else if (rc === 1) {  // Directory.
 57         if (exts === undefined)
 58           //目錄中是否存在 package.json
 59           //通過package.json文件,返回相應路徑
 60           exts = Object.keys(Module._extensions);
 61         filename = tryPackage(basePath, exts, isMain);
 62       }
 63 
 64       // 如果嘗試了上面都沒有得到filename 匹配所有擴展名進行嘗試,是否存在
 65       if (!filename) {
 66         // try it with each of the extensions
 67         if (exts === undefined)
 68           exts = Object.keys(Module._extensions);
 69         // 該模塊文件加上后綴名js .json .node進行嘗試,是否存在
 70         filename = tryExtensions(basePath, exts, isMain);
 71       }
 72     }
 73 
 74     // 如果仍然沒有得到filename,並且路徑類型是文件夾
 75     if (!filename && rc === 1) {  // Directory.
 76       if (exts === undefined)
 77         // 目錄中是否存在 package.json
 78         // 通過package.json文件,返回相應路徑
 79         exts = Object.keys(Module._extensions);
 80       filename = tryPackage(basePath, exts, isMain);
 81     }
 82 
 83     // 如果仍然沒有得到filename,並且路徑類型是文件夾
 84     if (!filename && rc === 1) {  // Directory.
 85       // try it with each of the extensions at "index"
 86       // 是否存在目錄名 + index + 后綴名
 87       // 嘗試 index.js index.json index.node
 88       if (exts === undefined)
 89         exts = Object.keys(Module._extensions);
 90 
 91       //tryExtensions()頭部定義方法,用來檢查文件加上js node json后綴是否存在
 92       filename = tryExtensions(path.resolve(basePath, 'index'), exts, isMain);
 93     }
 94 
 95 
 96     if (filename) {
 97       // Warn once if '.' resolved outside the module dir
 98       if (request === '.' && i > 0) {
 99         if (!warned) {
100           warned = true;
101           process.emitWarning(
102             'warning: require(\'.\') resolved outside the package ' +
103             'directory. This functionality is deprecated and will be removed ' +
104             'soon.',
105             'DeprecationWarning', 'DEP0019');
106         }
107       }
108 
109       // 將找到的文件路徑存入返回緩存,然后返回
110       Module._pathCache[cacheKey] = filename;
111       return filename;
112     }
113   }
114 
115   // 所以從這里可以看出,對於具體的文件的優先級:
116   // 1. 具體文件。
117   // 2. 加上后綴。
118   // 3. package.json
119   // 4  index加上后綴
120   // 可能的路徑以當前文件夾,nodejs系統文件夾和node_module中的文件夾為候選,以上述順序找到任意一個,
121   // 就直接返回
122 
123   // 沒有找到文件,返回false
124   return false;
125 };

 

Module._load 中還調用了 tryModuleLoad() 方法

 1 function tryModuleLoad(module, filename) {
 2   var threw = true;
 3 
 4   //try catch一下,如果裝載失敗,就會從cache中將這個模塊刪除。
 5   try {
 6 
 7     //做真正的導入模塊的操作
 8     module.load(filename);
 9     threw = false;
10   } finally {
11     if (threw) {
12       delete Module._cache[filename];
13     }
14   }
15 }

 

tryModuleLoad() 中調用了 Module.prototype.load() 方法

 1 // Given a file name, pass it to the proper extension handler.
 2 // 指定一個文件名,導入模塊,調用適當擴展處理函數,當前主要是js,json,和node
 3 Module.prototype.load = function(filename) {
 4   debug('load %j for module %j', filename, this.id);
 5 
 6   assert(!this.loaded); //斷言 確保當前模塊沒有被載入
 7   this.filename = filename; // 賦值當前模塊的文件名
 8 
 9   // Module._nodeModulePaths主要決定paths參數的值的方法。獲取node_modules文件夾所在路徑。
10   // path.dirname() 方法返回一個 path 的目錄名 path.dirname('/foo/bar/baz/asdf/quux')
11   // 返回: '/foo/bar/baz/asdf'
12   this.paths = Module._nodeModulePaths(path.dirname(filename));
13 
14   //當前文件的后綴
15   var extension = path.extname(filename) || '.js';
16 
17   //如果沒有后綴,默認為 .js
18   if (!Module._extensions[extension]) extension = '.js';
19 
20   //根據不同的后綴,執行不同的函數
21   Module._extensions[extension](this, filename);
22   this.loaded = true;
23 };

 

Module.prototype.load() 中調用了 Module._nodeModulePaths() 和 Module._extensions 方法

1、Module._nodeModulePaths() 根據操作系統的不同,返回不同的函數

 1 //path 模塊的默認操作會根據 Node.js 應用程序運行的操作系統的不同而變化。 
 2 //比如,當運行在 Windows 操作系統上時,path 模塊會認為使用的是 Windows 風格的路徑。
 3 //例如,對 Windows 文件路徑 C:\temp\myfile.html 使用 path.basename() 函數,
 4 //運行在 POSIX 上與運行在 Windows 上會產生不同的結果:
 5 //在 POSIX 上:
 6 //path.basename('C:\\temp\\myfile.html');
 7 // 返回: 'C:\\temp\\myfile.html'
 8 //
 9 // 在 Windows 上:
10 //path.basename('C:\\temp\\myfile.html');
11 // 返回: 'myfile.html'
12 //
13 // 以下就是根據不同的操作系統返回不同的路徑格式 ,具體可以了解http://nodejs.cn/api/path.html
14 //
15 //
16 // Module._nodeModulePaths主要決定paths參數的值的方法。獲取node_modules文件夾所在路徑。
17 // 'node_modules' character codes reversed
18 var nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ];
19 var nmLen = nmChars.length;
20 if (process.platform === 'win32') {
21   // 'from' is the __dirname of the module.
22   Module._nodeModulePaths = function(from) {
23     // guarantee that 'from' is absolute.
24     from = path.resolve(from);
25 
26     // note: this approach *only* works when the path is guaranteed
27     // to be absolute.  Doing a fully-edge-case-correct path.split
28     // that works on both Windows and Posix is non-trivial.
29 
30     // return root node_modules when path is 'D:\\'.
31     // path.resolve will make sure from.length >=3 in Windows.
32     if (from.charCodeAt(from.length - 1) === 92/*\*/ &&
33         from.charCodeAt(from.length - 2) === 58/*:*/)
34       return [from + 'node_modules'];
35 
36     const paths = [];
37     var p = 0;
38     var last = from.length;
39     for (var i = from.length - 1; i >= 0; --i) {
40       const code = from.charCodeAt(i);
41       // The path segment separator check ('\' and '/') was used to get
42       // node_modules path for every path segment.
43       // Use colon as an extra condition since we can get node_modules
44       // path for dirver root like 'C:\node_modules' and don't need to
45       // parse driver name.
46       if (code === 92/*\*/ || code === 47/*/*/ || code === 58/*:*/) {
47         if (p !== nmLen)
48           paths.push(from.slice(0, last) + '\\node_modules');
49         last = i;
50         p = 0;
51       } else if (p !== -1) {
52         if (nmChars[p] === code) {
53           ++p;
54         } else {
55           p = -1;
56         }
57       }
58     }
59 
60     return paths;
61   };
62 } else { // posix
63   // 'from' is the __dirname of the module.
64   Module._nodeModulePaths = function(from) {
65     // guarantee that 'from' is absolute.
66     from = path.resolve(from);
67     // Return early not only to avoid unnecessary work, but to *avoid* returning
68     // an array of two items for a root: [ '//node_modules', '/node_modules' ]
69     if (from === '/')
70       return ['/node_modules'];
71 
72     // note: this approach *only* works when the path is guaranteed
73     // to be absolute.  Doing a fully-edge-case-correct path.split
74     // that works on both Windows and Posix is non-trivial.
75     const paths = [];
76     var p = 0;
77     var last = from.length;
78     for (var i = from.length - 1; i >= 0; --i) {
79       const code = from.charCodeAt(i);
80       if (code === 47/*/*/) {
81         if (p !== nmLen)
82           paths.push(from.slice(0, last) + '/node_modules');
83         last = i;
84         p = 0;
85       } else if (p !== -1) {
86         if (nmChars[p] === code) {
87           ++p;
88         } else {
89           p = -1;
90         }
91       }
92     }
93 
94     // Append /node_modules to handle root paths.
95     paths.push('/node_modules');
96 
97     return paths;
98   };
99 }

 

2、Module._extensions 方法

 1 // 根據不同的文件類型,三種后綴,Node.js會進行不同的處理和執行
 2 // 對於.js的文件會,先同步讀取文件,然后通過module._compile解釋執行。
 3 // 對於.json文件的處理,先同步的讀入文件的內容,無異常的話直接將模塊的exports賦值為json文件的內容
 4 // 對於.node文件的打開處理,通常為C/C++文件。
 5 // Native extension for .js
 6 Module._extensions['.js'] = function(module, filename) {
 7   // 同步讀取文件
 8   var content = fs.readFileSync(filename, 'utf8');
 9 
10   // internalModule.stripBOM()剝離 utf8 編碼特有的BOM文件頭,
11   // 然后通過module._compile解釋執行
12   module._compile(internalModule.stripBOM(content), filename);
13 };
14 
15 
16 // Native extension for .json
17 Module._extensions['.json'] = function(module, filename) {
18   // 同步的讀入文件的內容
19   var content = fs.readFileSync(filename, 'utf8');
20   try {
21     // internalModule.stripBOM()剝離 utf8 編碼特有的BOM文件頭,
22     // 然后將模塊的exports賦值為json文件的內容
23     module.exports = JSON.parse(internalModule.stripBOM(content));
24   } catch (err) {
25     // 異常處理
26     err.message = filename + ': ' + err.message;
27     throw err;
28   }
29 };
30 
31 
32 //Native extension for .node
33 Module._extensions['.node'] = function(module, filename) {
34   // 對於.node文件的打開處理,通常為C/C++文件。
35   return process.dlopen(module, path._makeLong(filename));
36 };

 

針對 .js 后綴的,在 Module._extensions 還調用了 module._compile() 方法

  1 // Resolved path to process.argv[1] will be lazily placed here
  2 // (needed for setting breakpoint when called with --debug-brk)
  3 var resolvedArgv;
  4 // Run the file contents in the correct scope or sandbox. Expose
  5 // the correct helper variables (require, module, exports) to
  6 // the file.
  7 // Returns exception, if any.
  8 // 此方法用於模塊的編譯。
  9 // 參數content 主要是模塊js文件的主要內容,filename 是js文件的文件名
 10 Module.prototype._compile = function(content, filename) {
 11   // Remove shebang
 12   // Shebang(也稱為 Hashbang )是一個由井號和嘆號構成的字符序列 #!
 13   var contLen = content.length;
 14   if (contLen >= 2) {
 15     // 如果content 開頭有Shebang
 16     if (content.charCodeAt(0) === 35/*#*/ &&
 17         content.charCodeAt(1) === 33/*!*/) {
 18       if (contLen === 2) {
 19         // Exact match
 20         content = '';
 21       } else {
 22         // Find end of shebang line and slice it off
 23         // 找到以shebang開頭的句子的結尾,並將其分開,留下剩余部分 賦值給content
 24         var i = 2;
 25         for (; i < contLen; ++i) {
 26           var code = content.charCodeAt(i);
 27           if (code === 10/*\n*/ || code === 13/*\r*/)
 28             break;
 29         }
 30         if (i === contLen)
 31           content = '';
 32         else {
 33           // Note that this actually includes the newline character(s) in the
 34           // new output. This duplicates the behavior of the regular expression
 35           // that was previously used to replace the shebang line
 36           content = content.slice(i);
 37         }
 38       }
 39     }
 40   }
 41 
 42   // create wrapper function
 43   // Module.wrap頭部引入,主要用來給content內容包裝頭尾,類似於
 44 //   (function (exports, require, module, __filename, __dirname) {
 45 //         -----模塊源碼 content-----
 46 //    });
 47   var wrapper = Module.wrap(content);
 48 
 49 // 包裝好的文本就可以送到vm中執行了,這部分就應該是v8引擎的事情,
 50 // runInThisContext將被包裝后的源字符串轉成可執行函數,runInThisContext的作用,類似eval
 51   var compiledWrapper = vm.runInThisContext(wrapper, {
 52     filename: filename,
 53     lineOffset: 0,
 54     displayErrors: true
 55   });
 56 
 57   var inspectorWrapper = null;
 58   // 處理debug模式,
 59   if (process._debugWaitConnect && process._eval == null) {
 60     if (!resolvedArgv) {
 61       // we enter the repl if we're not given a filename argument.
 62       if (process.argv[1]) {
 63         resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
 64       } else {
 65         resolvedArgv = 'repl';
 66       }
 67     }
 68 
 69     // Set breakpoint on module start
 70     if (filename === resolvedArgv) {
 71       delete process._debugWaitConnect;
 72       inspectorWrapper = getInspectorCallWrapper();
 73       if (!inspectorWrapper) {
 74         const Debug = vm.runInDebugContext('Debug');
 75         Debug.setBreakPoint(compiledWrapper, 0, 0);
 76       }
 77     }
 78   }
 79 
 80   // 獲取當前的文件的路徑
 81   var dirname = path.dirname(filename);
 82 
 83   //生成require方法
 84   var require = internalModule.makeRequireFunction(this);
 85 
 86   //依賴模塊
 87   var depth = internalModule.requireDepth;
 88   if (depth === 0) stat.cache = new Map();
 89   var result;
 90 
 91   //直接調用content經過包裝后的wrapper函數,將module模塊中的exports,生成的require,
 92   //this也就是新創建的module,filename, dirname作為參數傳遞給模塊
 93   //類似於
 94   //(function (exports, require, module, __filename, __dirname) {
 95 //       -----模塊源碼 content-----
 96 //  })( this.exports, require, this, filename, dirname);
 97   // 這就是為什么我們可以直接在module文件中,直接訪問exports, module, require函數的原因
 98   if (inspectorWrapper) {
 99     result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
100                               require, this, filename, dirname);
101   } else {
102     result = compiledWrapper.call(this.exports, this.exports, require, this,
103                                   filename, dirname);
104   }
105   if (depth === 0) stat.cache = null;
106   return result;
107 };

 

Module.prototype._compile 中調用了 Module.wrap 這個方法就是用了給 content 包裝的主要函數, 它來自頭部的引用:

 1 //Module.wrapper和Module.wrap的方法寫在下面,
 2 //給傳入進去的script也就是咱們的content --js文件內容套了一個殼,使其最后變成類似於如下的樣子:
 3 //
 4 //(function (exports, require, module, __filename, __dirname) {
 5 //         -----模塊源碼-----
 6 // });
 7 //
 8 // NativeModule.wrap = function(script) {
 9 //     return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
10 // };
11 
12 // NativeModule.wrapper = [
13 //     '(function (exports, require, module, __filename, __dirname) { ',
14 //     '\n});'
15 // ];
16 Module.wrapper = NativeModule.wrapper;
17 Module.wrap = NativeModule.wrap;

 

module.js 中還定義了一些其它的方法,在這里沒有寫出來,像 stat、readPackage、tryPackage、頭部引入的一些方法等,大家可以 從我的github( https://github.com/JiayiLi/node.js-module)上 clone 下來,放到 IDE 里跟着調用順序一步一步的看,都有詳細的注釋。
 
根據函數調用順序,總體梳理一下
 

                                                                       (圖一) 

現在咱們再看這個圖,梳理一下剛才的代碼,就清晰多了。 

 

                                                    (圖二)

最后,還有個問題 lib 目錄下的模塊文件,像 module.js 也沒有定義 require ,module,exports 這些變量,他們是如何使用的呢?
這是因為在引入核心模塊的時候也進行了頭尾包裝的過程。這里就要提到 lib/internal 文件夾下的 bootstrap_node.js,屬於 node 啟動文件。

 

在 bootstrap_node.js 中定義了一個 NativeModule 對象,用於加載核心模塊,如 module.js、http.js 等即 lib 文件夾下的 排除 lib/internal 目錄下的 js 模塊。

 

在這個 NativeModule 對象中也定義了 require 方法,compile 方法、wrap 方法(用於包裝頭尾)等 都和上面的 module.js 中的相應的方法意思是一樣的,可以下載源碼了解一下。

結論就是,node.js 通過 NativeModule 來對 module.js 、fs.js 等核心模塊進行包裝加載,所以它們里面也可以使用 require。 
 
最后還是建議從我的 github( https://github.com/JiayiLi/node.js-module)上 clone 下來 ,放到 ide 里,按照函數的調用一個一個看,看的過程中也對照着圖一、圖二理清思路。 
 
 
 

 

------------- 學會的知識也要時常review ------------
 
 
 
 
 
 


免責聲明!

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



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