NodeJs模块加载流程分析(require)

一、 开篇干货介绍


1. NodeJs中的模块

NodeJS采用CommonJS规范实现了模块系统,这种规范的核心就是require语句。

2. CommonJs规范

CommonJS规范规定了如何定义一个模块,如何导出一个模块,和模块中的变量函数,以及如何使用定义好的模块:

  • 在CommonJS规范中一个文件就是一个模块
  • 在CommonJS规范中每个文件中的变量函数都是私有的,对其他文件不可见的
  • 在CommonJS规范中每个文件中的变量函数必须通过exports暴露之后其它文件才可以使用
  • 在CommonJS规范中想要使用其它文件暴露的变量函数必须通过require()导入模块才可以使用

二、探索原因


对导出的两种方式,以及得到不同的结果产生好奇:

// a.js(非直接赋值)
exports.name = 'jojo'; // { name: "jojo" }
module.exports.name = 'jojo'; // { name: "jojo" }

// a.js(直接赋值)
exports = 'jojo'; // {}
module.exports = 'jojo'; // 'jojo'

// b.js
let aModule = require('./a.js')
console.log(aModule)

然后就看了下node官方require的加载流程,并理解其原理,才完全明白为什么!接下来贴整理的流程:

三、流程梳理


(重点:require方法返回的是module.exports的值)

流程汇总

四、源码分析


1. 导入自定义模块

let aModule = require('./a.js')
console.log(aModule)

2. 进入Node模块文件module.js中官方定义的require方法

function require(path) {
  // 1. 内部调用了Module构造函数通过原型(`prototype`)定义的require方法,且只要是这个构造函数的实例就可以调用这个方法,直接通过给构造函数添加静态方法的方式,实例是拿不到定义的方法的;
  return self.require(path)
}
Module.prototype.require = function(path) {
  // 2. 通过Module对象的静态_load方法加载模块文件
  return Module._load(path)
}

3. 进入Module._load这个方法里(重点方法,最终返回结果的方法

首先,提前定义好Module这个构造函数及缓存对象,下面有用到:

function Module(id, parent) {
  this.id = id; // 模块路径
  this.exports = {};  // 模块exports值
}

Module._cache = {}

Module._load方法:

Module._load = function(path) {
  // 3. 通过Module对象的静态_resolveFilename方法, 得到绝对路径并添加后缀名
  var filename = Module._resolveFilename(path)

  // 4. 根据路径判断是否有缓存
  var cachedModule = Module._cache[filename]
  // 4.1 如果有,就直接返回缓存中对应的module.exports
  if (cachedModule) {
    return cachedModule.exports
  }
  // 4.2 如果没有就创建一个新的Module模块对象并缓存起来,并继续加载模块
  var module = new Module(filename)
  Module._cache[filename] = module

  // 5. 利用tryModuleLoad方法加载模块
  tryModuleLoad(module, filename)

  // 6. 返回module.exports
  return module.exports
}

看到这儿,也就相当于require的返回值是module.exports

function require(path) {
   return module.exports
}

那接下来就看如何修改实例上的module.exports的值 ?

4. 进入tryModuleLoad这个方法中,调用Module的原型上的方法load

function tryModuleLoad(module, filename) {
  module.load(filename)
}

5. 进入Module.prototype.load方法中,取出模块后缀,并根据不同后缀从Module._extensions对象中查找不同方法并执行对应的方法,加载模块

Module.prototype.load = function(filename) {
  var extension = path.extname(filename)
  Module._extensions[extension](this, filename)
}

Module._extensions对象的内容:

Module._extensions = {
  '.json': function (module, filename) {
        // 读取文件内容
        var content = fs.readFileSync(filename, 'utf8')
        // 转换成对象,直接赋值给module.exports即可
        module.exports = JSON.parse(conten);
   },
  'js': function(module, filename) {
        // 读取文件内容
        var content = fs.readFileSync(filename, 'utf8')
        // 调用Module的静态方法_compile来处理文件内容
        module._compile(content, filename)
   }
}

6. 在Module.prototype._compile方法里将js内容包裹在一个字符串函数里,并执行(重点:最终是否改变module.exports

Module.prototype._compile = function(content, filename) {
  // 将js内容包裹在函数字符串里
  var wrapper = Module.wrap(content)
  // 执行包裹函数之后的代码, 拿到执行结果(String -- Function)
  var compiledWrapper = vm.runInThisContext(wrapper)

  // 利用call/apply执行包裹的函数, 修改module.exports的值
  var args = [this.exports, require, this, filename, dirname]
  var result = compiledWrapper.apply(this.exports, args)
  return result
}

/* 最终执行的函数
  (function (exports, require, module, __filename, __dirname) {   
      module.exports.name = "jojo"
      exports.name = "jojo"
      module.exports = "jojo"
      exports = "jojo"
  }).call(this.exports, this.exports, require, this, filename, dirname);
*/

Module.wrap包裹函数:

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

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

五、重点分析(以上四种导出写法的不同结果)





ok,整理完毕!

你可能感兴趣的:(NodeJs模块加载流程分析(require))