阅读 317

module.exports实现原理以及和exports到底是啥关系

node中的module.exports到底咋实现的?

具体思路:

  1. 解析出一个绝对路径来

2)根据绝对路径查找文件,没有加后缀的,添加后缀查找文件

  1. 查到文件情况下,根据文件名查内容。

    1.先去缓存中看一下这个文件是否存在,如果有,则返回缓存
    2.如果没有,则执行以下操作:
    i.创建一个模块 module ii.根据不同的后缀名,加载模块 iii.加载完成后,将内容存到缓存中。

上代码:

let fs = require('fs');
let path = require('path');
let vm = require('vm');
function req(moduleId) {
  let p = Module._resolveFileName(moduleId);// p是一个绝对路径
  if (Module._cacheModule[p]) { //从缓存中取
    // 模块不存在,如果有直接把exports对象返回即可
    return Module._cacheModule[p].exports;
  }
  let module = new Module(p); // 表示没有缓存就生成一个模块
  // 加载模块
  let content = module.load(p); // 加载模块
  Module._cacheModule[p] = module;
  module.exports = content; //module.exports = {name:'zfpx'};
  return module.exports
}
let a = req('./a.js');
复制代码

具体其余模块讲解:

1.Module._resolveFileName:用于解析绝对路径

Module._resolveFileName = function (moduleId) {
  let p =path.join(__dirname,moduleId);//绝对路径
  // 没有后缀我在加上后缀 如果传过来的有后缀就不用加了
  if (!path.extname(moduleId)) {
    let arr = Object.keys(Module._extensions);
    for (let i = 0; i < arr.length; i++) {
      let file = p + arr[i];
      try {
        fs.accessSync(file);//判断文件是否存在
        return file;
      } catch (e) {
        console.log(e);
      }
    }
  } else {
    return p;
  }
}
复制代码

2.Module._cacheModule是一个对象,用来存加载过的模块
key: 文件名,value: 模块名

Module._cacheModule = {}// 根据的是绝对路径进行缓存的
复制代码

3.let module = new Module(p); // 表示没有缓存就生成一个模块 其中p是文件名

function Module(p) {
  this.id = p; // 当前模块的标识,Module._cacheModule对象取缓存时,根据id判断缓存是否存在
  this.exports = {}; // 每个模块都有一个exports属性
}
复制代码

4.let content = module.load(p); // 加载模块 我们引入的文件可能时.js,.json,.node格式,load方法就是根据不同的后缀名,做不同方式的解析

// 模块加载的方法
Module.prototype.load = function (filepath) {
  // 判断加载的文件是json还是node或者是js
  let ext = path.extname(filepath); // 判断文件的后缀名是 .js/.json/.node
  let content = Module._extensions[ext](this); // content就是json的内容
  return content
}
复制代码

5.load方法的核心时Module._extensions,这个方法几乎是核心内容了它的作用是,获得文件内容

// 所有的加载策略
Module.wrapper = ['(function(exports,require,module){','\n})'];
Module._extensions = {
  '.js': function (module) {
      // 读取js文件 增加一个闭包了
      let script = fs.readFileSync(module.id,'utf8');
      let fn = Module.wrapper[0] + script + Module.wrapper[1];
      let ff = vm.runInThisContext(fn);

      //在module.exports环境下执行(function(exports,require,module){module.exports='zfpx'})函数,
      // 参数分别对应exports-->module.exports(暗含exports=module.exports),
      // require-->req,module
      ff.call(module.exports,module.exports,req,module); 
      return module.exports
  },
  '.json': function (module) {
    return JSON.parse(fs.readFileSync(module.id, 'utf8')); // 读取那个文件
  },
 
}
复制代码

重点来了,解析js方法我怎么看不懂了。不要急。

let fn = Module.wrapper[0] + script + Module.wrapper[1];这句是把文件的内容通过闭包包起来。 let ff = vm.runInThisContext(fn);
vm.runInThisContext()方法会创建一个独立的沙箱环境,以执行对参数fn的编译,运行并返回结果。 vm.runInThisContext()方法运行的代码没有权限访问本地作用域,但是可以访问Global全局对象。

ff.call(module.exports,module.exports,req,module);--->ff是:(function(exports,require,module)) 传入参数,执行闭包,这里解释下exports和module.exports的关系
传参对应关系:
exports---->module.exports
require ----> req
module ----> module

可见:
exports 是module.export 的一个别名; module.exports = exports

比如这里被引入的文件内容:

this === module.exports (true)   //this就是module.exports
module.exports = 'zfpx'; // 'zfpx'
复制代码

若将module.exports='zfpx'改为exports='ff';则结果为{}

本来exports = module.exports = {}
后来exports = 'zfpx',切断了exports
和module.exports的关系,而最后返回的是module.exports,因此结果是{}

若将module.exports='zfpx'改为exports.a = '1',则结果为{'a':1}

本来exports = module.exports = {}
后来exports.a = 'zfpx';
则module.exports = {a:'zfpx'}