目录
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 差异,我们经常得到的答案是:
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'
]
}
为了更直观的理解,请看下面例子:
如果我们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
如果我们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把这些信息拷贝过来(如下图)
知其然,还要知其所以然。所以接下来我们参考源码来实现一个简易版Common JS
我们把自己手写的model命名为MyModule,下面是该对象的构造函数,对应的属性相对于源码有所删减,对应属性的解释见代码注释部分
function MyModule(id=''){
this.id=id //此id其实就是我们require模块的路径
this.exports={} // 导出的东西放这里,初始化为空对象
this.loaded=false // 用来标识当前模块是否已经加载
}
先来一个涉及到的函数的总揽图:
涉及到实例/静态方法,建议按照本文函数介绍的顺序去看,逻辑更清晰
我们上面提到,require方法实际上是module对象的一个实例方法,实现很简单,实际上是调用了Model的_load()方法
MyModule.prototype.require=function(id){ //MyModel的id属性作为require函数的参数
return MyModule._load(id) //具体实现见下文
}
_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;
}
通过用户传入的require参数来解析到真正的文件地址的,源码中这个方法比较复杂,就不再赘述。
源码见node/lib/internal/modules/cjs/loader.js at c6b96895cc74bc6bd658b4c6d5ea152d6e686d20 · nodejs/node · GitHub
该方法是真正用来加载模块的方法
ps:注意_extensions函数中的this,指的是该module的实例,因为这是个实例方法
MyModule.prototype.load=function(filename){
//获取文件后缀
const extname=path.extname(filename);
//调用后缀名对应的函数来处理(见下)
MyModule._extensions[extname](this,filename);
this.loaded=true
}
源码见:_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)
}
是加载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模块官方文档