nodejs模塊加載機制

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

clipboard.png

4.模塊引入三步走github

1.路徑分析
2.文件定位
3.編譯執行

1.路徑分析
核心模塊:如http、fs、path等,速度僅次於緩存。
路徑形式的文件:以.或者..開始的相對路徑,以/開始的絕對路徑。
自定義模塊:不屬於核心模塊也不屬於路徑形式的標識符。它是一種特殊的文件模塊,多是一個文件或者包的形式。這類模塊的查找是最費時的,也是全部方式中最慢的一種。在定位時,會給出一個可能路徑的數組
沒有mac電腦,直接copy阮老師的圖 http://www.ruanyifeng.com/blo...
clipboard.png數據庫

clipboard.png

關鍵源碼
https://github.com/nodejs/nod...json

clipboard.png

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對象初始化代碼

clipboard.png

以前咱們講到,nodejs源碼中Javascript編寫的核心模塊都會經過V8附帶的js2c.py工具轉換成C++裏面的數組,生成node_natives.h頭文件。
當調用process.binding('natives')時候,該方法將經過js2c.py工具轉換出的字符串數組取出,而後從新轉換爲普通字符串,以對Javascript核心模塊進行編譯和執行。

clipboard.png

clipboard.png

clipboard.png

繼續在該文件中咱們能夠看到

clipboard.png

clipboard.png

在lib/module.js也是實現被v8工具轉換成c++數組,因此經過NativeModule.require('module')對module模塊進行編譯執行,而且獲取module模塊導出的Module對象,在執行該對象的runMain函數,回過頭來咱們看module.js

clipboard.png

原來當咱們在命令行執行 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-...

相關文章
相關標籤/搜索