從源碼分析Node之require

關於Node中Module這裏不多說了(node把每一個文件都看成一個Module 的實例),只看整個分析流程

本文這只是一種情況,如果是引用node內部模塊則是有些不同的流程
require開始

//E:\\SVNPRO\node_modules\***\bin\proxy\index.js
require('util/nodeVersion.js')

這裏執行之後會走到

function require(path) {
  try {
    exports.requireDepth += 1;
    return self.require(path);
  } finally {
    exports.requireDepth -= 1;
  }
}

requireDepth指的是引用的深度,重點是self.require(path)

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);
};

以上全部的path在該例子中值都是‘util/nodeVersion.js’,到了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) {
    return cachedModule.exports;
  }

  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  tryModuleLoad(module, filename);

  return module.exports;
};

這裏的request是‘util/nodeVersion.js’,parent是指最開始執行 require('util/nodeVersion.js') 代碼的文件,就是index.js,也是一個module(模塊),parent屬性的值是

這裏寫圖片描述

順便說一下Module,每一個module實例都有上圖這些屬性
children 數組:是該module require了哪些模塊
exports:就是該文件(模塊)開放給外部調用接口,module.exports=somethingToexports 中的somethingToexports
loaded:是否已經被加載過,加載過的模塊會添加到Module._cache
parent:require過該模塊的模塊
paths:指所有有可能包含該模塊的路徑,通常是從當前目錄到根目錄每一級目錄下的node_modules文件夾
resolveFilenameCache:指的是index.js中前面require過的已經引入的模塊,每引入一個模塊都會把它加到這個屬性中,後面會看到

var filename = Module._resolveFilename(request, parent, isMain);

這一句是根據模塊名和parent來獲取到該模塊的絕對地址

新開一個分支來看 Module._resolveFilename (sea-node.js)

//sea-node.js
Module._resolveFilename = function(request, parent) {
  var res;
  //request = request.replace(/\?.*$/, '') // remove timestamp etc.

  //性能優化
  if(parent.resolveFilenameCache){
    if(parent.resolveFilenameCache[request]){
      return parent.resolveFilenameCache[request]
    }
  }else{
    parent.resolveFilenameCache = {};
  }

  res = _resolveFilename(request, parent);

  parent.resolveFilenameCache[request] = res;

  return res;
}

該函數中,先檢查parent中resolveFilenameCache中是否包含要引入的模塊(就是request參數),如果有說明之前require過,直接返回即可
如果沒有,執行_resolveFilename(request, parent);,這個函數和上面的函數名字一樣,但是在不同文件,不是一個函數

再下一層分支看 Module._resolveFilename (module.js)

//module.js
Module._resolveFilename = function(request, parent, isMain) {
  if (NativeModule.nonInternalExists(request)) {
    return request;
  }

  var resolvedModule = Module._resolveLookupPaths(request, parent);
  var id = resolvedModule[0];
  var paths = resolvedModule[1];

  // look up the filename first, since that's the cache key.
  debug('looking for %j in %j', id, paths);

  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;
};

Module._resolveLookupPaths 函數是利用parent和request來得到所有可能的存放該模塊的地址(如C盤下的.node_modules),例如本例中返回的resolvedModule值爲
這裏寫圖片描述
數組第一個元素是request值,第二個元素是parent.paths和其他可能的存放模塊的地址的組合
Module._findPath函數做的事對paths數組進行遍歷,並對可能的文件(.js .json .node)後綴都進行測試,直到找到這個文件,返回的filename是該模塊的絕對路徑

之後這個函數運行結束,跳出來到上一個_resolveFilename(sea-node.js)中只剩最後一句parent.resolveFilenameCache[request] = res;,這是之前說過的,找到該模塊後就把它添加到parent的resolveFilenameCache中,說明已經引用過。
這樣再上一層到 _load 函數中,下面是按照順序尋找該模塊:
1. Module._cache:緩存中
2. NativeModule:Node的“土著”模塊(自帶的,例如http,fs等)
3. 上面兩個都沒有,再新建模塊 var module = new Module(filename, parent);,這時候module.exports裏面還啥都沒有,還並沒有讀取文件內容。

Module._cache[filename] = module; 這一句代碼則是將該模塊加到緩存中去,以便下次可以直接從緩存中獲取。

重點來了!!!

tryModuleLoad(module, filename); 這個函數包含了讀取該模塊文件內容;執行該函數會進入Module.load()

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;
};

通過path.extname() 獲取該模塊文件的擴展名,Module._extesions是一個Object,包含對應每一個後綴文件的處理函數(.js .json .node 三種),這裏是js文件所以我們直接進入該函數看

Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};

先讀取文件內容,然後調用module._compile處理content

Module.prototype._compile = function(content, filename) {
  // Remove shebang
  var contLen = content.length;
  if (contLen >= 2) {
    if (content.charCodeAt(0) === 35/*#*/ &&
        content.charCodeAt(1) === 33/*!*/) {
      if (contLen === 2) {
        // Exact match
        content = '';
      } else {
        // Find end of shebang line and slice it off
        var i = 2;
        for (; i < contLen; ++i) {
          var code = content.charCodeAt(i);
          if (code === 10/*\n*/ || code === 13/*\r*/)
            break;
        }
        if (i === contLen)
          content = '';
        else {
          // Note that this actually includes the newline character(s) in the
          // new output. This duplicates the behavior of the regular expression
          // that was previously used to replace the shebang line
          content = content.slice(i);
        }
      }
    }
  }
  // create wrapper function
  var wrapper = Module.wrap(content);
  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });
  var dirname = path.dirname(filename);
  var require = internalModule.makeRequireFunction.call(this);
  var args = [this.exports, require, this, filename, dirname];
  var depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result = compiledWrapper.apply(this.exports, args);
  if (depth === 0) stat.cache = null;
  return result;
};

函數比較長,只複製了一部分,這裏主要關注wrapper,這是對讀取的js文件內容進行了一個“包裝”,因爲作爲一個模塊代碼裏面肯帶會有module.exports ,那麼怎麼把exports的內容傳出來呢,其實他做的工作就是類似於:
文件內容:module.exports={name:"bob"}
在外面包裝一層匿名函數:

(function(exports,require,module){
  module.exports={name:"bob"}
})(module.exports,require,module);

這樣執行完了裏面exports的內容就傳出來了。(回到源代碼)

wrapper在content上加上頭尾之後(這時候還是字符串,需要變成可以執行的js函數,例如eval的功能),經過處理(V8引擎處理的)後得到的compiledWrapper就是js函數,可以看下它執行的代碼

var args = [this.exports, require, this, filename, dirname];
var result = compiledWrapper.apply(this.exports, args);

到這裏就結束了,接口內容已經放到module.exports中了,最後loaded變成true代表該模塊已加載過了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章