Q7: require 的模块加载机制是什么?模块缓存是如何工作的?

30 阅读4分钟

Node.js 面试题详细答案 - Q7

Q7: require 的模块加载机制是什么?模块缓存是如何工作的?

require 模块加载机制

基本流程
// 1. 解析模块路径
// 2. 检查模块缓存
// 3. 加载模块文件
// 4. 编译执行模块
// 5. 缓存模块
// 6. 返回模块导出
模块路径解析
// 1. 核心模块(如 fs, http)
const fs = require('fs')

// 2. 相对路径模块
const utils = require('./utils')
const config = require('../config')

// 3. 绝对路径模块
const app = require('/path/to/app')

// 4. 第三方模块(从 node_modules)
const express = require('express')
const lodash = require('lodash')
模块查找顺序
// 模块查找顺序:
// 1. 核心模块
// 2. 当前目录的 node_modules
// 3. 父目录的 node_modules
// 4. 继续向上查找,直到根目录
// 5. 全局 node_modules

// 示例目录结构:
// /project
//   /node_modules
//     /express
//   /src
//     /utils
//       /node_modules
//         /lodash
//     /app.js

// 在 app.js 中:
const express = require('express') // 从 /project/node_modules 查找
const lodash = require('lodash') // 从 /project/src/utils/node_modules 查找

模块缓存机制

缓存的基本原理
// 模块缓存存储在 require.cache 中
console.log('缓存对象:', require.cache)

// 第一次加载模块
const module1 = require('./my-module')
console.log('第一次加载:', module1)

// 第二次加载同一模块(从缓存获取)
const module2 = require('./my-module')
console.log('第二次加载:', module2)

// 验证是否是同一个对象
console.log('是否相同:', module1 === module2) // true
缓存示例
// my-module.js
console.log('模块被加载了')
let count = 0

module.exports = {
  increment: () => ++count,
  getCount: () => count,
}

// main.js
console.log('=== 第一次加载 ===')
const module1 = require('./my-module')
console.log('计数:', module1.getCount()) // 1

console.log('=== 第二次加载 ===')
const module2 = require('./my-module')
console.log('计数:', module2.getCount()) // 1 (不是 2)

// 输出:
// 模块被加载了
// === 第一次加载 ===
// 计数: 1
// === 第二次加载 ===
// 计数: 1
缓存键值
// 缓存键是模块的绝对路径
const path = require('path')

console.log('当前文件路径:', __filename)
console.log('缓存键:', path.resolve(__filename))

// 检查模块是否在缓存中
const cacheKey = path.resolve('./my-module.js')
console.log('是否在缓存中:', cacheKey in require.cache)

模块加载详细过程

1. 路径解析
// 相对路径解析
const path = require('path')

function resolveModulePath(modulePath) {
  if (modulePath.startsWith('./') || modulePath.startsWith('../')) {
    return path.resolve(path.dirname(__filename), modulePath)
  }
  return modulePath
}

// 示例
console.log('解析路径:', resolveModulePath('./utils'))
console.log('解析路径:', resolveModulePath('../config'))
2. 文件扩展名处理
// require 会自动添加扩展名
// 查找顺序:.js, .json, .node

// 这些调用是等价的:
const config1 = require('./config')
const config2 = require('./config.js')
const config3 = require('./config.json')
3. 模块编译执行
// 不同类型的模块处理方式不同

// JavaScript 模块
// 1. 读取文件内容
// 2. 包装函数
// 3. 执行函数
// 4. 返回 module.exports

// JSON 模块
// 1. 读取文件内容
// 2. JSON.parse()
// 3. 返回解析结果

// 示例:JSON 模块
// config.json
{
  "name": "my-app",
  "version": "1.0.0"
}

// 使用
const config = require('./config.json');
console.log(config.name); // "my-app"

模块缓存管理

查看缓存
// 查看所有缓存的模块
console.log('缓存的模块:')
Object.keys(require.cache).forEach((key) => {
  console.log(key)
})

// 查看特定模块的缓存
const modulePath = require.resolve('./my-module')
console.log('模块路径:', modulePath)
console.log('是否在缓存中:', modulePath in require.cache)
清除缓存
// 清除特定模块的缓存
const modulePath = require.resolve('./my-module')
delete require.cache[modulePath]

// 重新加载模块
const freshModule = require('./my-module')
console.log('重新加载的模块:', freshModule)
热重载示例
// 实现模块热重载
function hotReload(modulePath) {
  const resolvedPath = require.resolve(modulePath)

  // 清除缓存
  delete require.cache[resolvedPath]

  // 重新加载
  return require(modulePath)
}

// 使用示例
let myModule = require('./my-module')

// 监听文件变化
const fs = require('fs')
fs.watchFile('./my-module.js', () => {
  console.log('模块文件变化,重新加载...')
  myModule = hotReload('./my-module')
})

循环依赖处理

循环依赖示例
// a.js
console.log('a.js 开始加载')
const b = require('./b')
console.log('a.js 中 b 的值:', b)
module.exports = { name: 'a' }

// b.js
console.log('b.js 开始加载')
const a = require('./a')
console.log('b.js 中 a 的值:', a)
module.exports = { name: 'b' }

// main.js
const a = require('./a')
console.log('main.js 中 a 的值:', a)

// 输出:
// a.js 开始加载
// b.js 开始加载
// b.js 中 a 的值: {} (空对象,因为 a.js 还没完全加载完成)
// a.js 中 b 的值: { name: 'b' }
// main.js 中 a 的值: { name: 'a' }
避免循环依赖
// 使用延迟加载避免循环依赖
// a.js
console.log('a.js 开始加载')
module.exports = {
  name: 'a',
  getB: () => require('./b'), // 延迟加载
}

// b.js
console.log('b.js 开始加载')
module.exports = {
  name: 'b',
  getA: () => require('./a'), // 延迟加载
}

性能优化

模块预加载
// 预加载常用模块
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')

// 这些模块会被缓存,后续使用更快
条件加载
// 根据条件动态加载模块
function loadModule(condition) {
  if (condition) {
    return require('./heavy-module')
  } else {
    return require('./light-module')
  }
}

总结

  • 加载机制:路径解析 → 缓存检查 → 文件加载 → 编译执行 → 缓存存储
  • 缓存机制:基于绝对路径的缓存,避免重复加载
  • 查找顺序:核心模块 → 相对路径 → node_modules
  • 文件类型:支持 .js, .json, .node 文件
  • 循环依赖:通过缓存机制处理,但可能导致不完整导出
  • 性能优化:利用缓存机制,合理设计模块结构