Node.js 模块加载机制与缓存
理解模块加载机制和缓存,有助于优化性能和避免循环依赖问题。
模块加载流程
当 require 一个模块时,Node.js 按以下步骤处理:
- 解析模块路径
- 检查缓存
- 加载模块内容
- 包装模块代码
- 执行模块代码
- 返回 exports 对象
模块路径解析
JavaScript
// 核心模块:直接加载
const fs = require('fs'); // 加载内置 fs 模块
const path = require('path'); // 加载内置 path 模块
// 相对路径:基于当前文件
const utils = require('./utils'); // ./utils.js
const config = require('../config'); // ../config.js
// 绝对路径:直接加载
const mod = require('/home/user/module.js');
// 第三方模块:查找 node_modules
const express = require('express'); // 从 node_modules 加载
node_modules 查找顺序
JavaScript
// 加载 'lodash' 时的查找顺序
// ./node_modules/lodash
// ../node_modules/lodash
// ../../node_modules/lodash
// ... 直到根目录或全局 node_modules
查看查找路径:
JavaScript
console.log(module.paths);
// [
// '/home/user/project/node_modules',
// '/home/user/node_modules',
// '/home/node_modules',
// '/node_modules'
// ]
模块缓存机制
模块首次加载后会被缓存,后续 require 返回缓存实例:
JavaScript
// counter.js
let count = 0;
module.exports = {
increment: () => ++count,
getCount: () => count
};
// app.js
const counter1 = require('./counter');
counter1.increment(); // count = 1
const counter2 = require('./counter');
console.log(counter2.getCount()); // 1(同一实例)
console.log(counter1 === counter2); // true
查看缓存
JavaScript
// 查看所有缓存模块
console.log(require.cache);
// 查看特定模块缓存
const modulePath = require.resolve('./utils');
console.log(require.cache[modulePath]);
清除缓存
JavaScript
// 清除单个模块缓存
delete require.cache[require.resolve('./config')];
// 重新加载模块
const config = require('./config'); // 重新执行模块代码
// 清除所有缓存(谨慎使用)
Object.keys(require.cache).forEach(key => {
delete require.cache[key];
});
循环依赖
JavaScript
// a.js
const b = require('./b');
console.log('a.js: b.name =', b.name);
exports.name = 'A';
// b.js
const a = require('./a'); // a.js 未完成,得到部分 exports
console.log('b.js: a.name =', a.name); // undefined
exports.name = 'B';
// main.js
const a = require('./a');
// 输出: b.js: a.name = undefined
// 输出: a.js: b.name = B
Node.js 处理循环依赖:返回模块未完成时的 exports 副本。
避免循环依赖
JavaScript
// 方案1:延迟加载
// a.js
exports.name = 'A';
exports.getB = () => require('./b'); // 延迟到调用时加载
// 方案2:提取公共模块
// common.js
exports.shared = { /* 共享数据 */ };
// a.js 和 b.js 都引入 common.js
require.resolve
JavaScript
// 解析模块路径,不加载模块
const path = require.resolve('./utils');
console.log(path); // /home/user/project/utils.js
// 用于检查模块是否存在
try {
require.resolve('./config');
console.log('模块存在');
} catch (err) {
console.log('模块不存在');
}
模块包装
Node.js 会将模块代码包装在函数中:
JavaScript
// 原模块代码
const a = 1;
module.exports = a;
// 实际执行(包装后)
(function(exports, require, module, __filename, __dirname) {
const a = 1;
module.exports = a;
});
这解释了为什么 module、exports、require、__filename、__dirname 在模块中可用。
要点总结
- 模块首次加载后缓存,后续 require 返回缓存实例
- node_modules 从当前目录向上查找直到根目录
- 循环依赖返回未完成的 exports 副本,应避免
- require.resolve 解析路径但不加载模块
- 模块代码被包装,exports/require/module 等是注入参数
📝 发现内容有误?点击此处直接编辑