核心模块
function MyModule(id = '') {this.id = id; // 这个id其实就是我们require的路径this.path = path.dirname(id); // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径this.exports = {}; // 导出的东西放这里,初始化为空对象this.filename = null; // 模块对应的文件名this.loaded = false; // loaded用来标识当前模块是否已经加载}
以上是模块加载的核心模块。我们每require一个模块,都会创造一个上面这种模块实例对象。那么我们在执行require的时候,有哪些步骤呢?答案就在_load方法中。
MyModule._cache = Object.create(null);MyModule._load = function (request) {// step1:获取文件绝对路径以及完整文件名const filename = MyModule._resolveFilename(request);// step2:检查缓存,如果有缓存直接返回const cachedModule = MyModule._cache[filename];if (cachedModule !== undefined) {return cachedModule.exports;}// step3:构造模块实例对象,并加载模块const module = new MyModule(filename);MyModule._cache[filename] = module;module.load(filename);return module.exports;}// require的本质MyModule.prototype.require = function (id) {return MyModule._load(id);}
可以看到,当我们调用require方法时,我们拿到的实际上是_load方法的返回值,而这个返回值就是实例对象上的exports属性。在_load中,一共有三步。我们接下来会详细的讲解这三步。
step1:获取文件绝对路径以及完整文件名
我们来看_resolveFilename是怎么实现的
MyModule._extensions = Object.create(null);MyModule._resolveFilename = function (request) {const filename = path.resolve(request); // 获取传入参数对应的绝对路径const extname = path.extname(request); // 获取文件后缀名// 如果没有文件后缀名,尝试添加.js和.jsonif (!extname) {const exts = Object.keys(MyModule._extensions);for (let i = 0; i < exts.length; i++) {const currentPath = `${filename}${exts[i]}`;// 如果拼接后的文件存在,返回拼接的路径if (fs.existsSync(currentPath)) {return currentPath;}}}return filename;}
我们在写require的时候,常常把文件扩展名省略掉了。但是在源码处理当中实际上是又给我们加上了。这个方法实际作用就是得到文件的绝对路径以及完整的文件名,并返回。如果文件扩展名被省略了,则将当前文件名和可支持的文件扩展名拼接,并搜索是否存在。最后返回拼接后的文件名。
step2:检查缓存,如果有缓存直接返回
step3:加载模块
这是模块加载的重头戏,先是创建模块实例对象,再把它加入缓存。这没啥好讲的,重点关注load方法。
MyModule.prototype.load = function (filename) {// 获取文件后缀名const extname = path.extname(filename);// 调用后缀名对应的处理函数来处理MyModule._extensions[extname](this, filename);this.loaded = true;}MyModule._extensions['.js'] = function (module, filename) {const content = fs.readFileSync(filename, 'utf8'); // 同步读文件module._compile(content, filename);}MyModule._extensions['.json'] = function (module, filename) {const content = fs.readFileSync(filename, 'utf8');module.exports = JSON.parse(content);}
这部分内容比较多,我们按照顺序来讲。
load方法作用是什么?作用就是根据不同类型的文件,执行不同的加载策略。require支持js,json以及node格式的文件加载。本文只讨论js和json的加载。这里,我们再次遇到了MyModule._extensions这个对象。这个对象的key是类型名,value是处理该类型文件的方法。
对于JSON文件,我们读文件拿到内容后,直接使用JSON.parse就可以将读取的文件变为json对象形式。并将处理后的内容赋给module.exports。
而对于js文件,我们的处理要更复杂一些。
js文件的处理
对于js的文件处理,主要集中在_compile这个方法中。
MyModule.wrapper = ['(function (exports, require, module, __filename, __dirname) { ','\n});'];MyModule.wrap = function (script) {return MyModule.wrapper[0] + script + MyModule.wrapper[1];};MyModule.prototype._compile = function (content, filename) {const wrapper = MyModule.wrap(content); // 获取包装后函数体// vm是nodejs的虚拟机模块,runInThisContext方法可以接受一个字符串并将它转化为一个函数// 返回值就是转化后的函数,所以compiledWrapper是一个函数const compiledWrapper = vm.runInThisContext(wrapper, {filename,lineOffset: 0,displayErrors: true,});// 准备exports, require, module, __filename, __dirname这几个参数// exports可以直接用module.exports,即this.exports// require官方源码中还包装了一层,其实我们这里可以直接使用this.require// module不用说,就是this了// __filename直接用传进来的filename参数了// __dirname需要通过filename获取下const dirname = path.dirname(filename);compiledWrapper.call(this.exports, this.exports, this.require, this,filename, dirname);}
compile方法第一步就是将之前读取的js文件内容,包裹为一个方法。这样做的好处一是可以通过运行方法的方式执行一遍文件内部代码,从而拿到导出的值。另一个好处就是隔离var这种全局变量,避免全局污染。
包装为方法后,此时还是字符串,要变成可执行的方法体。要么通过eval实现,要么就用vm.runInThisContext实现,这里使用后者实现。之后调用call。执行方法。
关键来了,这里的参数很关键,其中一个就是export参数
此时传进去的export是形参,其地址和module.exports指向相同(因为是引用类型嘛)。但是内部不可将export直接赋值. 如: export = 1这样就会丢失module.exports的引用。
完整代码
const path = require('path');const vm = require('vm');const fs = require('fs');function MyModule(id = '') {this.id = id; // 这个id其实就是我们require的路径this.path = path.dirname(id); // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径this.exports = {}; // 导出的东西放这里,初始化为空对象this.filename = null; // 模块对应的文件名this.loaded = false; // loaded用来标识当前模块是否已经加载}MyModule._cache = Object.create(null);MyModule._extensions = Object.create(null);MyModule._load = function (request) { // request是我们传入的路劲参数const filename = MyModule._resolveFilename(request);// 先检查缓存,如果缓存存在且已经加载,直接返回缓存const cachedModule = MyModule._cache[filename];if (cachedModule !== undefined) {return cachedModule.exports;}// 如果缓存不存在,我们就加载这个模块// 加载前先new一个MyModule实例,然后调用实例方法load来加载// 加载完成直接返回module.exportsconst module = new MyModule(filename);// load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整MyModule._cache[filename] = module;module.load(filename);return module.exports;}/*** 获取完整文件名(带绝对路径)* @param request 文件名* @returns {string} 完整文件名(带绝对路径)* @private*/MyModule._resolveFilename = function (request) {const filename = path.resolve(request); // 获取传入参数对应的绝对路径const extname = path.extname(request); // 获取文件后缀名// 如果没有文件后缀名,尝试添加.js和.jsonif (!extname) {const exts = Object.keys(MyModule._extensions);for (let i = 0; i < exts.length; i++) {const currentPath = `${filename}${exts[i]}`;// 如果拼接后的文件存在,返回拼接的路径if (fs.existsSync(currentPath)) {return currentPath;}}}return filename;}MyModule.prototype.require = function (id) {return MyModule._load(id);}MyModule.prototype.load = function (filename) {// 获取文件后缀名const extname = path.extname(filename);// 调用后缀名对应的处理函数来处理MyModule._extensions[extname](this, filename);this.loaded = true;}MyModule._extensions['.js'] = function (module, filename) {const content = fs.readFileSync(filename, 'utf8'); // 同步读文件module._compile(content, filename);}MyModule.wrapper = ['(function (exports, require, module, __filename, __dirname) { ','\n});'];MyModule.wrap = function (script) {return MyModule.wrapper[0] + script + MyModule.wrapper[1];};MyModule.prototype._compile = function (content, filename) {const wrapper = MyModule.wrap(content); // 获取包装后函数体// vm是nodejs的虚拟机模块,runInThisContext方法可以接受一个字符串并将它转化为一个函数// 返回值就是转化后的函数,所以compiledWrapper是一个函数const compiledWrapper = vm.runInThisContext(wrapper, {filename,lineOffset: 0,displayErrors: true,});// 准备exports, require, module, __filename, __dirname这几个参数// exports可以直接用module.exports,即this.exports// require官方源码中还包装了一层,其实我们这里可以直接使用this.require// module不用说,就是this了// __filename直接用传进来的filename参数了// __dirname需要通过filename获取下const dirname = path.dirname(filename);compiledWrapper.call(this.exports, this.exports, this.require, this,filename, dirname);}MyModule._extensions['.json'] = function (module, filename) {const content = fs.readFileSync(filename, 'utf8');module.exports = JSON.parse(content);}// 起始require,源码没有这个。function myRequire(id) {return MyModule._load(id);}const a = myRequire('./a.js');const add = myRequire('./b.js');const c = myRequire('./c.js');const c_copy = myRequire('./c.js');const d = myRequire('./d.js');const e = myRequire('./e.json');console.log(a);console.log(add(1, 2));console.log(c);console.log(c_copy);console.log(d);
后记
我这里实现的只是简单版的requre,真正的requre的流程如下:
- 优先加载内置模块,即使有同名文件,也会优先使用内置模块。
- 不是内置模块,先去缓存找。
- 缓存没有就去找对应路径的文件。
- 不存在对应的文件,就将这个路径作为文件夹加载。
- 对应的文件和文件夹都找不到就去node_modules下面找。
- 还找不到就报错了。
我只是实现了其中的2 3 而已。
