使用

  1. 定义暴露模块

    1. module.exports = value;
    2. exports.xxx = value;
    3. // const exports = modules.exports;
  2. 引入模块:

    1. var module = require(模块名或模块路径);

    模块加载机制

    require 某个模块时,会从头执行该模块文件的代码,module.exports = xxx 也只是其中的一句执行代码,但该结果会缓存到一个全局对象中

  1. // c.js
  2. let c = 1;
  3. c = c + 1;
  4. module.exports = c;
  5. c = 6;
  1. const c = require('./c.js');
  2. console.log(c); // c的值是2
  1. {
  2. "c.js": 2,
  3. }

加载顺序

5caa00510001e43408210581.jpg

实现 require

  1. 路径分析
  2. 文件定位
  3. 编译执行

    路径分析和文件定位

    ```javascript MyModule._cache = 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.exports const module = new MyModule(filename);

// load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整 MyModule._cache[filename] = module;

module.load(filename);

return module.exports; }

  1. ```javascript
  2. MyModule._resolveFilename = function (request) {
  3. const filename = path.resolve(request); // 获取传入参数对应的绝对路径
  4. const extname = path.extname(request); // 获取文件后缀名
  5. // 如果没有文件后缀名,尝试添加.js和.json
  6. if (!extname) {
  7. const exts = Object.keys(MyModule._extensions);
  8. for (let i = 0; i < exts.length; i++) {
  9. const currentPath = `${filename}${exts[i]}`;
  10. // 如果拼接后的文件存在,返回拼接的路径
  11. if (fs.existsSync(currentPath)) {
  12. return currentPath;
  13. }
  14. }
  15. }
  16. return filename;
  17. }

编译执行

为了注入 exports, require, module, __dirname, __filename 这几个变量,将原本的代码包裹进一个匿名函数中

  1. MyModule.wrapper = [
  2. '(function (exports, require, module, __filename, __dirname) { ',
  3. '\n});'
  4. ];
  5. MyModule.wrap = function (script) {
  6. return MyModule.wrapper[0] + script + MyModule.wrapper[1];
  7. };
  1. MyModule.prototype._compile = function (content, filename) {
  2. const wrapper = Module.wrap(content); // 获取包装后函数体
  3. // vm是nodejs的虚拟机沙盒模块,runInThisContext方法可以接受一个字符串并将它转化为一个函数
  4. // 返回值就是转化后的函数,所以compiledWrapper是一个函数
  5. const compiledWrapper = vm.runInThisContext(wrapper, {
  6. filename,
  7. lineOffset: 0,
  8. displayErrors: true,
  9. });
  10. // 准备exports, require, module, __filename, __dirname这几个参数
  11. // exports可以直接用module.exports,即this.exports
  12. // require官方源码中还包装了一层,其实最后调用的还是this.require
  13. // module不用说,就是this了
  14. // __filename直接用传进来的filename参数了
  15. // __dirname需要通过filename获取下
  16. const dirname = path.dirname(filename);
  17. // 第一个 this.exports 说明我们 js 文件里面 this 是对 module.exports 的一个引用
  18. // 第二个 this.exports 说明 js 文件里面的 exports 也是对 module.exports 的一个引用
  19. compiledWrapper.call(this.exports, this.exports, this.require, this,
  20. filename, dirname);
  21. }

之后就是拿到上面的代码执行

参考资料

  1. 深入 Node.js 的模块加载机制,手写 require 函数
  2. node 技术栈