Node.js 模块机制原理简介

Node.js的模块机制可以让我们很方便地将js代码按照功能进行封装。在一个模块中我们使用require()方法引入另一个模块,使用module.exports向外暴露方法、对象、变量供其他模块引用。新建.js文件的时候,并没有引入require方法和module变量,这些是从哪里来的呢?

module对象

在 Node.js 模块系统中,每个文件都被视为独立的模块,并且有一个module对象与之对应。举个栗子,下面是circle.js的内容

// circle.js
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
console.log(module);

module打印出来,看看是什么内容,运行node circle.js
这里写图片描述
可以看到打印出来一个对象,类名是Module,包含了id,exports,filename,parent,children等属性,每个属性解释如下:

  1. module.id 模块的标识符。 通常是完全解析后的文件名
  2. module.exports 一个对象,包含了模块对外暴露的方法、局部变量、类等
  3. module.filename 模块的完全解析后的文件名。
  4. module.parent 最先引用该模块的模块。
  5. module.children 被该模块引用的模块对象。

Tips:exports.area 是 module.exports.area的简单写法

新建一个foo.js文件,引用circle.js文件

//foo.js
const circle = require('./circle.js');
console.log(`半径为 4 的圆的面积是 ${circle.area(4)}`);

看下运行效果:
这里写图片描述
可以看到,通过require()方法,可以成功引用circle.js方法中暴露的area()方法。并且foo.js对应的module对象成为了circle.js对应circle对象的parent(父摸块)。

module对象从何而来

module对象在circle.js中并没有引用就直接使用了,它从哪来的?
有兴趣的同学可以看下面的源码分析,否则可以直接跳到总结。

源码分析

node/module.js源码可以看下引入一个文件的时候Node.js做了什么。

// Loads a module at the given file path. Returns that module's
// `exports` property.
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);
};

require方法在校验了路径参数是否合法后,将路径参数传给了加载方法 _load()

// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
//    filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }

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

  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;

  var hadException = true;

  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  return module.exports;
};

可以看到对新引入的文件,在_load()方法中新建了module对象,然后调用了新建对象的load()方法,参数是被引入的文件名(解析后的),返回结果是新建module对象的exports属性

// Given a file name, pass it to the proper extension handler.
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;
};

这里会根据不同文件类型,引用不同的加载方法,我们仅以js文件为例

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

调用_compile()方法,传入文件名和文件内容

// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
Module.prototype._compile = function(content, filename) {
  // remove shebang
  content = content.replace(shebangRe, '');

  // create wrapper function
  var wrapper = Module.wrap(content);

  var compiledWrapper = runInThisContext(wrapper,
                                      { filename: filename, lineOffset: 0 });
  if (global.v8debug) {
    if (!resolvedArgv) {
      // we enter the repl if we're not given a filename argument.
      if (process.argv[1]) {
        resolvedArgv = Module._resolveFilename(process.argv[1], null);
      } else {
        resolvedArgv = 'repl';
      }
    }

    // Set breakpoint on module start
    if (filename === resolvedArgv) {
      // Installing this dummy debug event listener tells V8 to start
      // the debugger.  Without it, the setBreakPoint() fails with an
      // 'illegal access' error.
      global.v8debug.Debug.setListener(function() {});
      global.v8debug.Debug.setBreakPoint(compiledWrapper, 0, 0);
    }
  }
  const dirname = path.dirname(filename);
  const require = internalModule.makeRequireFunction.call(this);
  const args = [this.exports, require, this, filename, dirname];
  const depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  const result = compiledWrapper.apply(this.exports, args);
  if (depth === 0) stat.cache = null;
  return result;
};

在_compile()方法中可以看到,js文件内容先被wrap(包装)了一下,然后才使用runInThisContext来运行包装后的代码。我们看下wrap()方法是怎么包装的

  NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
  };

  NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
  ];

可以看到就是讲引入文件的首尾加了些代码,讲内容包在一个function里。回到前面的_compile方法,传入runInThisContext()方法的参数都是新建module对象的属性或对象本身, require是使用module作为上下文新建的方法。

总结

使用require()方法引入一个.js文件模块,简单说经历了下面过程:
1,Node首先根据文件名看下缓存中有没有module对象,如果有就返回,没有就新建一个module对象并保存在缓存中。
2,加载文件内容后,Node使用一个如下的函数包装器将其包装:

(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});

3, 使用新建的module对象作为上下文,整理参数传入上面包装好的函数
4, 返回module.exports属性

参考文献

1,https://www.jianshu.com/p/609489e8c929#
2,http://nodejs.cn/api/modules.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值