1. commonjs为啥模块导出的基本数据类型修改后不变,引用类型修改后改变
counter.js
let count = 1;
let obj = {
count: 1
}
function increment () {
count++;
obj.count++;
}
module.exports = {
count,
obj,
increment
}
main.js
const counter = require('./counter.js');
counter.increment();
console.log(counter.count); // 1
console.log(counter.obj.count); // 2
- 模块要导出的count是number基本类型值、test是一个引用类型的值
- 模块要导出的increment是一个方法,可以修改模块要导出供别人使用的值count和obj的count属性
- main.js中调用require加载counter.js后,就会生成模块的缓存,缓存其实不会重新读取js文件,直接返回内存中的module.exports对象
- 如果模块中导出的某个变量是基本类型变量如count,那么module.exports对象会对count进行值的拷贝,也就是内存中重新开辟一块内存,把count值的拷贝过来
- counter.increment()调用之后修改的counter.js模块的内部count变量的值,不会修改module.exports对象的count变量的值,因为commonjs没有living binding的功能;这也就是ESModule改进的点,让模块的值的变化能让其他模块感知到,读取到最新的值
- commonjs没有living binding的功能,为啥obj.count就变了呢?因为commonjs如果导出引用数据类型时会进行浅拷贝该对象;module.exports中的obj和模块内部的obj指向了同一份内存,所以及时在模块内部修改obj的count,也能反应到module.exports.obj上
2. commonjs通过require后的模块修改基本数据类型后为啥值就变化了
counter.js
let count = 1;
let obj = {
count: 1
}
function increment () {
count++;
obj.count++;
}
module.exports = {
count,
obj,
increment
}
main.js
const counter = require('./counter.js');
// counter.increment();
counter.count++;
counter.obj.count++;
console.log(counter.count); // 2
console.log(counter.obj.count); // 2
- main.js模块中counter的count和obj的count都改变,很好理解,因为改的就是counter的变量的值
- 为啥下面main2.js中的值也会变化呢?
main2.js
require('./main.js')
const counter = require('./counter.js');
console.log(counter.count); // 2
console.log(counter.obj.count); // 2
- require('./counter.js')会从缓存中直接读取模块,返回的是仅有的一份module.exports对象,只要该模块被加载过一次,module.exports对象每次就取到的是一个对象;
- module.exports对象每取到和main.js的counter是一个对象,所以值就改变了
- 如果main.js中还是通过counter.increment();就拿不到最新的counter值了,因为没改module.exports对象中的counter
3. commonjs通过require后的模块解构后为啥不能修改基本数据类型的变量
main3.js
const {count, obj} = require('./counter.js');
count++; // TypeError: Assignment to constant variable.
obj.count++;
console.log(count);
console.log(obj.count); // 2
- require的 {count, obj} 是真正的结构,ESModule不是解构
- 解构后count是一个const类型的值了,所以不能改
- 解构后修改obj.count仍然是修改module.exports对象中的obj,因为是同一份引用
4.commonjs模块中直接module.exports一个变量的情况
const.js
var a = 1;
module.exports = a;
const-test.js
let a = require("./const.js");
a++;
console.log(a)
- 能直接导出变量,不用加{}
- 如果⽤户导出的是⼀个变量,后续变量被修改了,是否会影响导⼊的值?
// exportsA.js
let a = 0
setInterval(() => {
a++
}, 1000)
module.exports = a // 如果导出的是引⽤类型则会产⽣变化。
// useExportsA.js
setInterval(() => {
let a = require("./exportsA")
//多次require拿到是第一个,第一个值如果是对象,对象中的内容变化了可以得到更新,如果是具体的值 则不会变化
console.log(a) // require会缓存上次导⼊的结果。
}, 1000)
- 和上面1里面的情况一样,修改模块中基本类型变量,不会living bing到已经生成的module.exports上;除非直接改module.exports
5. commonjs模块的模拟实现
简化的 require
// 1) 内部采用了同步读取的方案 ,将文件内容获取到。 require方法是同步的。 底层用的是同步的读取 fs.readFileSync
function require(文件名) {
// 根据文件名获取内容,包装函数,并且把获取到的结果放到module.exports
let module = {
exports: "",
}
;(function (module, exports) {
module.exports = "结果"
})(module, module.exports)
return module.exports
}
const fs = require("fs")
const path = require("path")
const vm = require("vm")
function Module(id) {
this.id = id
this.exports = {} // 模块最终的导出的结果都在这里面
}
Module._cache = {} // 模块缓存
Module._extensions = {
".js"(module) {
const content = fs.readFileSync(module.id, "utf8")
// 如何将字符串包装成函数, 可以用new Function来直接实现
// console.log(new Function('a','b','c',content).toString())
// eval 找的是上层作用域 可以直接将代码放到这个eval转成成js
// 支持沙箱 可以保证作用域不污染,可以指定函数中的this,手动指定上下文
// 快照, 通过proxy来实现沙箱
const wrapperFunction = vm.compileFunction(content, [
"module",
"exports",
"require",
"__dirname",
"__filename",
])
let require = req
let __dirname = path.dirname(module.id) // 文件对应的目录
let __filename = module.id // 绝对路径
Reflect.apply(wrapperFunction, exports, [
module,
exports,
require,
__dirname,
__filename,
])
},
".json"(module) {
// json如何处理
const content = fs.readFileSync(module.id, "utf8")
module.exports = JSON.parse(content)
},
}
Module._resolveFilename = function (id) {
// 默认会查找同名的文件,会尝试添加后缀
const exts = Reflect.ownKeys(Module._extensions)
let isExisits = fs.existsSync(id) // 不会抛错 fs.access 需要用tryCatch
if (isExisits) return path.resolve(__dirname, id)
// 先查找js在查找json
for (let i = 0; i < exts.length; i++) {
let fileUrl = path.resolve(__dirname, id) + exts[i]
if (fs.existsSync(fileUrl)) {
return fileUrl
}
}
throw new Error("模块未找到")
}
Module.prototype.load = function () {
const ext = path.extname(this.id) // a.min".js"
Module._extensions[ext](this) // 根据后缀名来处理对应的模块
}
function req(id) {
// 1.根据用户传递的id 来进行模块的加载,相对路径转换成绝对路径
let absPath = Module._resolveFilename(id)
// 2.创建模块
let existsModule = Module._cache[absPath] // 是否存在这个模块
if (existsModule) {
return existsModule.exports // 返回上一次导出的结果
}
const module = new Module(absPath) // 如果我多次require模块这个模块只会被读取一次
Module._cache[absPath] = module
// 3.就是加载这个模块
module.load() // 加载完模块后既可以拿到最终的模块导出结果
return module.exports
}
const result = req("./b.json")
console.log(result)
基本实现
-
- Module._load 加载这个模块
-
- Module._resolveFilename( 处理路径为绝对路径, 并且添加文件后缀
-
- 拿到文件 看一下文件是否加载过 Module._cache 是否缓存过,如果缓存过则直接结束
-
- 如果没有缓存过 则会new Module(id,exports = {}) exports 是对应模块的导出结果,默认为空
-
- 将创建的模块缓存
-
- 根据文件加载模块 (给module.exports 赋值)
-
- 找到对应的文件后缀 做加载操作 Module._extensions[.js](this, filename); 策略模式
-
- 读取文件内容 fs.readFileSync(filename, 'utf8');
-
- 将字符串执行 module._compile 编译字符串
-
- 包裹函数 'exports','require','module','__filename', '__dirname', module.exports = exports this = exports
-
- Reflect.apply(this,[exports,require,module,filename,path.dirname]) module.exports = 'abc' 最终返回的是 module.exports