一、 开篇干货介绍
1. NodeJs中的模块
NodeJS采用CommonJS规范实现了模块系统,这种规范的核心就是require
语句。
2. CommonJs规范
CommonJS规范规定了如何定义一个模块,如何导出一个模块,和模块中的变量函数,以及如何使用定义好的模块:
- 在CommonJS规范中一个文件就是一个模块
- 在CommonJS规范中每个文件中的变量函数都是私有的,对其他文件不可见的
- 在CommonJS规范中每个文件中的变量函数必须通过exports暴露之后其它文件才可以使用
- 在CommonJS规范中想要使用其它文件暴露的变量函数必须通过require()导入模块才可以使用
二、探索原因
对导出的两种方式,以及得到不同的结果产生好奇:
// a.js(非直接赋值)
exports.name = 'jojo'; // { name: "jojo" }
module.exports.name = 'jojo'; // { name: "jojo" }
// a.js(直接赋值)
exports = 'jojo'; // {}
module.exports = 'jojo'; // 'jojo'
// b.js
let aModule = require('./a.js')
console.log(aModule)
然后就看了下node官方require的加载流程,并理解其原理,才完全明白为什么!接下来贴整理的流程:
三、流程梳理
(重点:require
方法返回的是module.exports
的值)
四、源码分析
1. 导入自定义模块
let aModule = require('./a.js')
console.log(aModule)
2. 进入Node模块文件module.js
中官方定义的require
方法
function require(path) {
// 1. 内部调用了Module构造函数通过原型(`prototype`)定义的require方法,且只要是这个构造函数的实例就可以调用这个方法,直接通过给构造函数添加静态方法的方式,实例是拿不到定义的方法的;
return self.require(path)
}
Module.prototype.require = function(path) {
// 2. 通过Module对象的静态_load方法加载模块文件
return Module._load(path)
}
3. 进入Module._load
这个方法里(重点方法,最终返回结果的方法
)
首先,提前定义好Module这个构造函数及缓存对象,下面有用到:
function Module(id, parent) {
this.id = id; // 模块路径
this.exports = {}; // 模块exports值
}
Module._cache = {}
Module._load
方法:
Module._load = function(path) {
// 3. 通过Module对象的静态_resolveFilename方法, 得到绝对路径并添加后缀名
var filename = Module._resolveFilename(path)
// 4. 根据路径判断是否有缓存
var cachedModule = Module._cache[filename]
// 4.1 如果有,就直接返回缓存中对应的module.exports
if (cachedModule) {
return cachedModule.exports
}
// 4.2 如果没有就创建一个新的Module模块对象并缓存起来,并继续加载模块
var module = new Module(filename)
Module._cache[filename] = module
// 5. 利用tryModuleLoad方法加载模块
tryModuleLoad(module, filename)
// 6. 返回module.exports
return module.exports
}
看到这儿,也就相当于require的返回值是
module.exports
function require(path) {
return module.exports
}
那接下来就看如何修改实例上的
module.exports
的值 ?
4. 进入tryModuleLoad
这个方法中,调用Module的原型上的方法load
function tryModuleLoad(module, filename) {
module.load(filename)
}
5. 进入Module.prototype.load
方法中,取出模块后缀,并根据不同后缀从Module._extensions
对象中查找不同方法并执行对应的方法,加载模块
Module.prototype.load = function(filename) {
var extension = path.extname(filename)
Module._extensions[extension](this, filename)
}
Module._extensions
对象的内容:
Module._extensions = {
'.json': function (module, filename) {
// 读取文件内容
var content = fs.readFileSync(filename, 'utf8')
// 转换成对象,直接赋值给module.exports即可
module.exports = JSON.parse(conten);
},
'js': function(module, filename) {
// 读取文件内容
var content = fs.readFileSync(filename, 'utf8')
// 调用Module的静态方法_compile来处理文件内容
module._compile(content, filename)
}
}
6. 在Module.prototype._compile
方法里将js内容包裹在一个字符串函数里,并执行(重点:最终是否改变module.exports
)
Module.prototype._compile = function(content, filename) {
// 将js内容包裹在函数字符串里
var wrapper = Module.wrap(content)
// 执行包裹函数之后的代码, 拿到执行结果(String -- Function)
var compiledWrapper = vm.runInThisContext(wrapper)
// 利用call/apply执行包裹的函数, 修改module.exports的值
var args = [this.exports, require, this, filename, dirname]
var result = compiledWrapper.apply(this.exports, args)
return result
}
/* 最终执行的函数
(function (exports, require, module, __filename, __dirname) {
module.exports.name = "jojo"
exports.name = "jojo"
module.exports = "jojo"
exports = "jojo"
}).call(this.exports, this.exports, require, this, filename, dirname);
*/
Module.wrap
包裹函数:
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) {',
'\n});'
];
Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1]
}
五、重点分析(以上四种导出写法的不同结果)
ok,整理完毕!