CommonJS模块和ES6模块的区别?深入CommonJS源码,带你手写自己的CommonJS

目录

ES6 CommonJS 差异

小试牛刀:打印一下

加深理解:举几个例子

例子1:基本数据类型

例子2:引用类型

手写实现Common JS

module构造函数

module方法

MyModule.prototype.require()

MyModule._load

MyModule._resolveFilename()

MyModule.prototype.load()

MyModule._extensions['X']

MyModule.prototype._compile()


ES6 CommonJS 差异

说到ES6 CommonJS 差异,我们经常得到的答案是:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

那么该如何理解这两句话呢?

在这之前,我们首先要明确两个概念:

commonJS中: module/require,本质是对象/函数,并非JS关键字

而ES6 的import/export则是是JS关键字

小试牛刀:打印一下

如果我们直接将module还有require打印出来,就会出现下面的内容:不难发现,这两个其实就是对象和函数。

其中,module对象里面的export属性,就是我们平时用的module.exports。那么其他属性都有什么含义呢?

 module 中的几个属性:

  • exports:这就是 module.exports 对应的值,由于还没有赋任何值给它,它目前是一个空对象。
  • loaded:表示当前的模块是否加载完成。

require 函数中的属性:

  • main 指向当前当前引用自己的模块。
  • extensions 表示目前 node 支持的几种加载模块的方式。
  • cache 表示 node 中模块加载的缓存,当一个模块加载一次后,之后 require 不会再加载一次,而是从缓存中读取。
[Function: require] {
  resolve: [Function: resolve] { paths: [Function: paths] },
  //指向引用自己的module
  main: Module {
    id: '.',
    path: '/Users/lee/Desktop/code/test',
    exports: {},
    filename: '/Users/lee/Desktop/code/test/test1.js',
    loaded: false,
    children: [],
    paths: [
      '/Users/lee/Desktop/code/test/node_modules',
      '/Users/lee/Desktop/code/node_modules',
      '/Users/lee/Desktop/node_modules',
      '/Users/lee/node_modules',
      '/Users/node_modules',
      '/node_modules'
    ]
  },
 //表示目前 node 支持的几种加载模块的方式
  extensions: [Object: null prototype] {
    '.js': [Function (anonymous)],
    '.json': [Function (anonymous)],
    '.node': [Function (anonymous)]
  },
  //module缓存
  cache: [Object: null prototype] {
    '/Users/lee/Desktop/code/test/test1.js': Module {
      id: '.',
      path: '/Users/lee/Desktop/code/test',
      exports: {},
      filename: '/Users/lee/Desktop/code/test/test1.js',
      loaded: false,
      children: [],
      paths: [Array]
    }
  }
}
Module {
  id: '.', 
  path: '/Users/lee/Desktop/code/test',
  exports: {},
  filename: '/Users/lee/Desktop/code/test/test1.js',
  loaded: false,
  children: [],
  paths: [
    '/Users/lee/Desktop/code/test/node_modules',
    '/Users/lee/Desktop/code/node_modules',
    '/Users/lee/Desktop/node_modules',
    '/Users/lee/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}

加深理解:举几个例子

为了更直观的理解,请看下面例子:

例子1:基本数据类型

如果我们modules.exports的是基本数据类型,对其进行如下操作,大家猜一下结果是多少

//test1.js
var num=1;
function setNum(newNum){
  num=newNum
}
setTimeout(() => {
    console.log(num);//2
  }, 5000)
module.exports={
  num,
  setNUm
}

//test2.js
var {num,setNum}=require('./test1.js')
console.log(num) //1
setNum(2)
console.log(num) //1
//输出结果顺序:
//1
//1
//2

以上代码可以等价于下面的代码,这样看,是不是觉得很容易理解了


var num=1;
function setNum(newNum){
  num=newNum
}
setTimeout(() => {
    console.log(num);//2
  }, 5000)
module.exports={
  num,
  setNUm
}

var {num:useNum,setNum:useSetNum}=module.exports
console.log(useNum) // 1
setNum(2)
console.log(useNum) //1

例子2:引用类型

如果我们modules.exports的是基本数据类型,对其进行如下操作,结果又是多少

//test1.js
var obj={
  a:1,
  b:2
};
function setVal(newA){
  obj.a=newA
}
setTimeout(() => {
    console.log(obj.a);//{ a: 2, b: 2 }
  }, 5000)
module.exports={
  obj,
  setVal
}

//test2.js
var {obj,setVal}=require('./test1.js')
console.log(obj) //{ a: 1, b: 2 }
useSetVal(2)
console.log(obj) //{ a: 2, b: 2 }
//最后输出结果顺序
// { a: 1, b: 2 }
// { a: 2, b: 2 }
// { a: 2, b: 2 }

以上代码等价于

var obj={
  a:1,
  b:2
};
function setVal(newA){
  obj.a=newA
}
setTimeout(() => {
    console.log(obj.a);//{ a: 2, b: 2 }
  }, 5000)
module.exports={
  obj,
  setVal
}

//test2.js
var {obj:useObj,setVal:useSetVal}=require('./test1.js')
console.log(useObj) //{ a: 1, b: 2 }
useSetVal(2)
console.log(useObj) //{ a: 2, b: 2 }

通过上面两个例子,我们可以更直观的感受到这样的事实:CommonJS中module/require,本质是对象/函数

所以Common JS本质上就是我们把需要的信息传到module.exports,然后通过require把这些信息拷贝过来(如下图)

CommonJS模块和ES6模块的区别?深入CommonJS源码,带你手写自己的CommonJS_第1张图片

手写实现Common JS

知其然,还要知其所以然。所以接下来我们参考源码来实现一个简易版Common JS

module构造函数

我们把自己手写的model命名为MyModule,下面是该对象的构造函数,对应的属性相对于源码有所删减,对应属性的解释见代码注释部分


function MyModule(id=''){
  this.id=id             //此id其实就是我们require模块的路径
  this.exports={}        // 导出的东西放这里,初始化为空对象
  this.loaded=false      // 用来标识当前模块是否已经加载
}

module方法

先来一个涉及到的函数的总揽图:

CommonJS模块和ES6模块的区别?深入CommonJS源码,带你手写自己的CommonJS_第2张图片

涉及到实例/静态方法,建议按照本文函数介绍的顺序去看,逻辑更清晰

MyModule.prototype.require()

 我们上面提到,require方法实际上是module对象的一个实例方法,实现很简单,实际上是调用了Model的_load()方法

MyModule.prototype.require=function(id){  //MyModel的id属性作为require函数的参数
  return MyModule._load(id)        //具体实现见下文
}

MyModule._load

_load方法是require方法真正的主体部分,它都干了点啥呢?

  • 先检查该模块是否已经在缓存中,若在,则直接返回缓存模块的exports

  • 若不在,就new 一个Module实例,用该实例加载对应模块(这样module.exports对象才有值),并且缓存,返回该模块的exports

缓存直接放在Module._cache这个静态变量上,这个变量官方初始化使用的是Object.create(null),这样可以使创建出来的原型指向null


MyModule._cache = Object.create(null);

MyModule._load()=function (id){   //id就是传入的模块路径参数
  const filename=MyModule._resolveFilename(id)   //解析获得真正的模块地址(见下)
  const cachedModule=MyModule._cache[filename];
  // 当缓存存在,就直接返回这个模块的缓存
  if(cachedModule!==undefined){
    return cachedModule.exports;
  }
  // 缓存不存在,new module实例并返回
  const module=new MyModule(filename)
  // 缓存该module
  MyModule._cache[fileName]=module

  module.load(filename);       //真正用来加载模块的方法(见下)
  return module.exports;
  
}

MyModule._resolveFilename()

通过用户传入的require参数来解析到真正的文件地址的,源码中这个方法比较复杂,就不再赘述。

源码见node/lib/internal/modules/cjs/loader.js at c6b96895cc74bc6bd658b4c6d5ea152d6e686d20 · nodejs/node · GitHub

MyModule.prototype.load()

该方法是真正用来加载模块的方法

ps:注意_extensions函数中的this,指的是该module的实例,因为这是个实例方法

MyModule.prototype.load=function(filename){
  //获取文件后缀
  const extname=path.extname(filename);
  
  //调用后缀名对应的函数来处理(见下)
  MyModule._extensions[extname](this,filename);

  this.loaded=true
}

MyModule._extensions['X']

源码见:_extension源码

该方法是用来处理不同类型的文件的,X可以是.js .JSON .node三种

不同文件类型的处理方法都挂载在MyModule._extensions上面

本文只实现.js

例:MyModule._extensions['.js"](module1, filename);就是处理.js文件的

MyModule._extensions['.js']=function(module,filename){
  //这一步很简单,就是将文件内容读出来
  const content=fs.readFileSync(filename,'utf8')

  // 这个方法才是核心,详见下文
  module._compile(content,filename)
}

MyModule.prototype._compile()

是加载JS核心的所在,它都干了点啥呢:

1.会将目标文件拿出来包裹一层来注入exports,require,module,__dirname,__filename,以便我们在js文件里面能直接使用这几个变量

2.之后将文件执行一遍。

如何实现这种注入呢?执行的时候在代码外加一层函数,将我们想要的变量作为函数参数

例子

//如果源文件内容是这样
module.exports = "hello world";

//那么这样加一层函数,就实现了我们上面提到的注入
function (module) { // 注入module变量,其实几个变量同理
  module.exports = "hello world";
}

_compile具体实现:

//把包裹的函数还有参数写成字符串写到数组里
MyModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

//把源文件代码包裹起来的函数
MyModule.wrap=function(code){
  return MyModule.wrap[0]+code+MyModule.wrapper[1];
}

MyModule.prototype._compile=function(content,filename){
  const wrapper =Module.wrap(content);   //获取包装后的函数体

  // vm是nodejs的虚拟机沙盒模块
  //runInThisContext方法可以接受一个字符串并将它转化为一个函数
  // 返回值就是转化后的函数,所以compiledWrapper是一个函数
  const compiledWrapper = vm.runInThisContext(wrapper, {
    filename,
    lineOffset: 0,
    displayErrors: true,
  });
  
  // 准备exports, require, module, __filename, __dirname这几个参数传到函数里
  
  const dirname = path.dirname(filename);
  //用call执行该函数
  compiledWrapper.call(this.exports, this.exports, this.require, this,filename, dirname);
}

 参考:

1.深入浅出 ESM 模块 和 CommonJS 模块

2.深入Node.js的模块加载机制

3.Node.js模块官方文档

你可能感兴趣的:(javascript,es6,前端,node.js)