从源码分析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代表该模块已加载过了

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