模块的查找

绝对路径

直接去 绝对路径 找模块加载,找不到则报错;

相对路径

相对于当前模块,将 相对路径./a../a)转化成 绝对路径 加载模块;

模块名(mono-repo)

  1. 先检查是否是内置模块(如:fspath 等),如果是直接使用;

  2. 检查当前目录中的 node_modules 是否包含 模块名,找到后转成 绝对路径 加载模块,找不到继续查找;

  3. 检查上级目录中的 node_modules 是否包含 模块名,找到后转成 绝对路径 加载模块,找不到继续查找;

  4. 找到顶级后,找到则加载,找不到则报错;

没有后缀的文件

若模块没有后缀名,则会按照 模块.js模块.json模块.node(用不到)、模块.mjs 的顺序加载;

目录

如果仅提供目录,不提供文件名,则自动寻找该目录下的 index.js

包名

  1. 如果仅提供 包名 ,若当前目录中的 node_modules 包含 包名 这个模块,会找 包名 下的 package.json 入口文件中的 main 字段去加载这个文件,找到后转成 绝对路径 加载模块,找不到继续查找;

  2. 检查上级目录中的 node_modules 是否包含 模块名,找到后转成 绝对路径 加载模块;

  3. 找到顶级后,找到则加载,找不到则报错;

module 对象

  1. a.js

    module.exports = 1111
    console.log(module)
    
  2. node a.js 的输出结果

    // 记录了当前模块的信息
    Module {
      // id:返回模块标识符,一般是一个绝对路径,如果是入口文件则是一个 .
      id: '.',
    
      // 当前模块的目录
      path: '/Users/wushuai/Desktop/node基础/05Module',
    
      // 返回当前模块需要暴露的内容
      exports: 'lg',
    
      // 存放调用当前模块的父模块
      parent: {},
    
      // 当前模块的文件名称,也是绝对路径;
      filename: '/Users/wushuai/Desktop/node基础/05Module/m.js',
      
      // loaded:返回布尔值,表示模块是否完成加载;
      loaded: false,
    
      // 当前模块调用的子模块
      children: [],
    
      // 返回当前模块所在目录和上级所有目录拼接 node_modules 的绝对路径,可以用于分析 Node 中模块加载的具体位置
      paths: [
        '/Users/wushuai/Desktop/node基础/05Module/node_modules',
        '/Users/wushuai/Desktop/node基础/node_modules',
        '/Users/wushuai/Desktop/node_modules',
        '/Users/wushuai/node_modules',
        '/Users/node_modules',
        '/node_modules'
      ]
    }
    

require 函数

  1. a.js

    console.log(require);
    
  2. node a.js 的输出结果

    [Function: require] {
      // resolve 方法会把路径转换成 绝对路径
      resolve: [Function: resolve] { paths: [Function: paths] },
    
      // 主模块,入口模块
      main: Module {
        id: '.',
        path: '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src',
        exports: {},
        parent: null,
        filename: '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src/index.js',
        loaded: false,
        children: [],
        paths: [
          '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src/node_modules',
          '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/node_modules',
          '/Users/wushuai/Desktop/frontend-node-master/node_modules',
          '/Users/wushuai/Desktop/node_modules',
          '/Users/wushuai/node_modules',
          '/Users/node_modules',
          '/node_modules'
        ]
      },
    
      // 扩展名
      extensions: [Object: null prototype] {
        '.js': [Function],
        '.json': [Function],
        '.node': [Function],
        '.mjs': [Function]
      },
    
      // 目前已经缓存的模块
      cache: [Object: null prototype] {
        '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src/index.js': Module {
          id: '.',
          path: '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src',
          exports: {},
          parent: null,
          filename: '/Users/wushuai/Desktop/frontend-node-master/1-2. 全局对象/src/index.js',
          loaded: false,
          children: [],
          paths: [Array]
        },
      }
    }
    

模块加载流程

// 伪代码演示
function require(modulePath) {
    // 1. 将 modulePath 转换为绝对路径:D:\repository\NodeJS\源码\myModule.js

    // 2. 判断 require.cache 中该模块是否已有缓存
    if (require.cache["D:\\repository\\NodeJS\\源码\\myModule.js"]) {
      return require.cache["D:\\repository\\NodeJS\\源码\\myModule.js"].result;
    }

    // 3. 读取文件内容
    console.log("当前模块路径:", __dirname);
    console.log("当前模块文件:", __filename);
    exports.c = 3;
    module.exports = {
        a: 1,
        b: 2
    };
    this.m = 5;

    // 4. 包裹到一个函数中
    function __temp(exports, require, module, __dirname, __filename) {
        console.log("当前模块路径:", __dirname);
        console.log("当前模块文件:", __filename);
        exports.c = 3;
        module.exports = {
            a: 1,
            b: 2
        };
        this.m = 5;
    }

    // 5. 创建 module 对象
    module.exports = {};
    const exports = module.exports; // exports 是 module.exports 的引用地址

    // 6. 缓存加载过的模块
    require.cache["D:\\repository\\NodeJS\\源码\\myModule.js"] = module.exports;

    // 7. 编译执行
    __temp.call(module.exports /* this 指向 */, exports, require, module, module.path, module.filename);

    // 8. 返回模块
    return module.exports;
}

手写模块加载

本例以文件模块为例,模拟实现 Nodejs 的模块加载流程,以了解 Nodejs 的加载规则

const { dir } = require('console')
const fs = require('fs')
const path = require('path')
const vm = require('vm')

function Module(id) {
  this.id = id
  this.exports = {}
}

Module._resolveFilename = function (filename) {
  // 利用 Path 将 filename 转为绝对路径
  let absPath = path.resolve(__dirname, filename)

  // 只完成主要逻辑
  // 判断当前路径对应的内容是否存在
  if (fs.existsSync(absPath)) {
    // 如果条件成立则说明 absPath 对应的内容是存在的
    return absPath
  } else {
    // 如果不存在
    // 文件定位(尝试补足不同的后缀重新判断)
    // 先 .js .json .node补足,继续找
    // 若没有找到,则当成一个目录,找 package.json 中的 main 对应的路径继续补足后缀,还是不存在,则继续找 index,没有继续找
    // 把每一个 node module 都按照以上步骤找一遍
    // 这里只进行简单判断
    let suffix = Object.keys(Module._extensions)
    for (var i = 0; i < suffix.length; i++) {
      let newPath = absPath + suffix[i]
      if (fs.existsSync(newPath)) {
        return newPath
      }
    }
  }

  // 找不到抛出错误
  throw new Error(`${filename} is not exists`)
}

Module._extensions = {
  '.js'(module) {
    // 读取
    let content = fs.readFileSync(module.id, 'utf-8')
    // 包装
    content = Module.wrapper[0] + content + Module.wrapper[1]
    // VM 
    let compileFn = vm.runInThisContext(content)
    // 准备参数的值
    let exports = module.exports
    let dirname = path.dirname(module.id)
    let filename = module.id
    // 调用,expoets 默认就是 {},这也是为什么在 模块中打印 this 输出的是一个 {}
    compileFn.call(exports, exports, myRequire, module, filename, dirname)
  },
  '.json'(module) {
    let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))
    module.exports = content
  },
  // ......
}

Module.wrapper = [ "(function (exports, require, module, __filename, __dirname) {", "\n})" ]

Module._cache = {}

Module.prototype.load = function () {
  // 后缀名字
  let extname = path.extname(this.id)
  Module._extensions[extname](this)
}

function myRequire(filename) {
  // 1 绝对路径
  let mPath = Module._resolveFilename(filename)

  // 2 缓存优先
  let cacheModule = Module._cache[mPath]
  if (cacheModule) return cacheModule.exports

  // 3 创建空对象加载目标模块
  let module = new Module(mPath)

  // 4 缓存已加载过的模块
  Module._cache[mPath] = module

  // 5 执行加载(编译执行)
  module.load()

  // 6 返回数据
  return module.exports
}


let obj = myRequire('./js文件') //  引入 js文件.js
let obj2 = myRequire('./json文件') //  引入 json文件.json

面试题

输出结果

  1. 执行下面的代码,示例一

    console.log("当前模块路径:", __dirname);
    console.log("当前模块文件:", __filename);
    // exports 是 module.exports 的引用
    exports.c = 3;
    // module.exports 重新赋值,切断了 exports 的联系,module.exports 是新的引用(内存地址)
    module.exports = {
      a: 1,
      b: 2
    };
    // this 的指向是 exports
    this.m = 5;
    
    console.log(this === module.exports); // false,module.exports 重新赋值,是新的引用(内存地址)
    console.log(this === exports); // true
    console.log(module.exports); // { a: 1, b: 2 },重新赋值后的值
    
  2. 执行下面的代码,示例二

    console.log("当前模块路径:", __dirname);
    console.log("当前模块文件:", __filename);
    // exports 是 module.exports 的引用
    exports.a = 1;
    exports.b = 2;
    exports.c = 3;
    // this 的指向是 exports
    this.m = 5;
    
    console.log(this === module.exports); // true
    console.log(this === exports); // true
    console.log(module.exports); // { a: 1, b: 2, c: 3, m: 5 }
    

CommonJS 的循环引用

  1. 分析下列这段代码,并解释原理

    JS
    JS
    JS
    //main.js
    var a = require('./a')
    console.log(a)
    
    // a.js
    module.exports.a = 1
    var b = require('./b')
    console.log(b)
    module.exports.a = 2
    
    // b.js
    module.exports.b = 11
    var a = require('./a')
    console.log(a)
    module.exports.b = 22
    
  2. require 后的模块是会被缓存的,还需要注意的是先加入缓存,然后再执行,这样在按照代码同步的执行顺序去分析代码就会很清晰,具体分析如下:

    • 执行 require('./a') 会将 a 模块加入缓存,然后执行 a 模块中的内容,执行权交到了 a 模块中,执行 a
    • 执行第一行将缓存的 a 值赋值为 1 ,然后执行第二行 require('./b')b 模块加入缓存,并把执行权交到 b 模块中;
    • b 模块中把 b 的值赋值为 11 ,在 require('./a') 时,是从缓存中取的值,这里就会在控制台打印 {a: 1},最后把缓存中的 b 值修改为 22 ,执行权交给上一级;
    • 代码执行权回到 a 模块中,这时 b 从缓存中取的值是 22,控制台中打印 { b: 22 },最后把缓存中的 a 值修改为 2 ,执行权交给上一级;
    • 代码执行回到 main 模块中,这时缓存中的 a2 ,控制台中打印 { a: 2 },然后代码执行完毕;
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 模块的查找
    1. 1.1. 绝对路径
    2. 1.2. 相对路径
    3. 1.3. 模块名(mono-repo)
    4. 1.4. 没有后缀的文件
    5. 1.5. 目录
    6. 1.6. 包名
  2. 2. module 对象
  3. 3. require 函数
  4. 4. 模块加载流程
  5. 5. 手写模块加载
  6. 6. 面试题
    1. 6.1. 输出结果
    2. 6.2. CommonJS 的循环引用