3. commonjs模块的实现原理

模块

  • es6Module 静态导入 在编译的时候就可以知道使用了哪些变量 可以实现tree-shaking
  • commonjs 动态导入 不支持tree-shaking

commonjs规范

  • 想使用哪个模块就require谁 (后缀可以省略,默认会查找.js文件,没有js找.json)
  • 想被别人使用需要导出 module.exports
  • 在node中每一个js/json文件就是一个模块
  • 一个包中包含多个模块(每个包都必须配置一个package.json文件)

原理: 读取文件 => 包装自执行函数,设置参数 => 默认返回module.exports对象

运行流程分析

let a = require(“./del”);

  1. mod.require(path) -> Module.prototype.require
  2. Module._load 加载模块
  3. Module._resolveFilename 方法就是把路径变成绝对路径 添加后缀名
  4. 实现模块的缓存(根据绝对路径进行模块的缓存)
    • module.exports 会在第一次缓存起来(导出的结果如果是一个对象内部属性变了会跟着变, 普通值不会变)后续再去使用的话会取上次的返回值
    • es6模块使用export {} 导出的是一个变量,如果内部对应的值发生变化 导出的是会跟着变的
  5. 会尝试加载是不是一个原生模块,如果带相对路径或者绝对路径就不是核心模块
  6. 拿到绝对路径,创造一个模块 new Module。
    几个重要的属性: this.id this.exports = {} this.path 父路径
  7. module.load 对模块进行加载
  8. 根据文件后缀 去做策略加载
  9. 用的是同步读取
  10. 增加一个函数的壳子,并让函数执行,让 module.exports 作为了 this
  11. 用户会默认拿到 module.exports 的返回值
  12. 最终返回的是 exports 对象

node 中 commonjs 规范的简单实现

const fs = require('fs');
const path = require('path');
const vm = require('vm');

function Module(id) {
  // 传入的id是文件的绝对路径
  this.id = id;
  this.exports = {};
}
Module._cache = {};
Module._extensions = {
  '.js'(module) {
    // 读取脚本
    let script = fs.readFileSync(module.id, 'utf8');
    // 包装函数,生成字符串
    let template = `(function(exports, module, require, __dirname, __filename){${script}})`;
    // 字符串变成函数
    let fn = vm.runInThisContext(template); // 相当于 new Function
    // 函数执行,将this指向module.exports
    let exports = module.exports;
    let thisValue = exports; // this = module.exports = exports
    let filename = module.id;
    let dirname = path.dirname(filename);
    // 函数执行,调用了模块,也就完成了 module.exports的赋值
    fn.call(thisValue, exports, module, req, dirname, filename);
  },
  '.json'(module) {
    // 获取文件内容
    let script = fs.readFileSync(module.id, 'utf8');
    // 导出
    module.exports = JSON.parse(script);
  },
};
// 返回require文件的绝对路径
Module._resolveFilename = function (id) {
  let filePath = path.resolve(__dirname, id);
  console.log(filePath, 'filePath');

  // 通过判断filePath这个路径存不存在来看传入的路径有没有加后缀
  let isExists = fs.existsSync(filePath);
  if (isExists) return filePath;
  // 不存在 则尝试添加后缀
  let keys = Reflect.ownKeys(Module._extensions); // 拿到所有的key
  for (let i = 0; i < keys.forEach.length; i++) {
    let newPath = filePath + keys[i];
    if (fs.existsSync(newPath)) return newPath;
  }
  throw new Error('module not found');
};
// 加载模块,让用户给module.exports赋值
Module.prototype.load = function () {
  // 不需要传任何参数,this就指向创建的module实例
  // 先取文件后缀名
  let ext = path.extname(this.id);
  // 根据后缀名,采用不同的策略去加载
  Module._extensions[ext](this);
};
/*
自实现的require方法
filename是传入的文件路径
*/
function req(filename) {
  // 返回绝对路径,并且加上后缀
  filename = Module._resolveFilename(filename);
  if (Module._cache[filename]) {
    return Module._cache[filename].exports;
  }
  // 创造一个模块
  const module = new Module(filename);
  // 增加缓存
  Module._cache[filename] = module;
  // 对模块进行加载
  module.load(); // 就是让用户给module.exports赋值
  // 最终导出module.exports
  return module.exports; // 默认是空对象
}
// let a = require("./del");
let a = req('./del');

console.log(a, '输出');
复制代码

⚠️注意点

this = module.exports = exports
指向的是同一个引用地址 如果将exports更改了 module.exports不会变

  • 导出的时候 module.exports = “aaa”
  • 不能 exports = “aaa”;
  • 但是可以这样导出 exports.a = “111” this.b = “222”;(因为没有改变引用地址,和 module.exports 指向同样的引用地址)
  • 小结:最终用户使用的结果都是来自于module.exports; 不要同时使用exports和module.exports否则会以module.exports结果为基准
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享