导读:本期聚焦于小伙伴创作的《深入理解CommonJS模块加载机制:require函数是如何实现递归加载与缓存的》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《深入理解CommonJS模块加载机制:require函数是如何实现递归加载与缓存的》有用,将其分享出去将是对创作者最好的鼓励。

CommonJS是Node.js生态中广泛使用的模块规范,其核心通过require函数实现模块的引入,而require函数的递归加载和缓存机制是保证模块高效运行的关键。理解这两个机制,能帮助开发者更好地处理模块依赖、循环引用等场景。

深入理解CommonJS模块加载机制:require函数是如何实现递归加载与缓存的

CommonJS模块的基本结构

Node.js在执行CommonJS模块时,会将模块内容包裹在一个函数中,这个函数会接收exportsrequiremodule等参数,模块内部的变量不会污染全局作用域。我们可以通过以下代码查看模块的原始结构:

// 打印当前模块的包装函数字符串
console.log(require('module').wrapper);

输出的结果是一个数组,第一个元素是函数开头,第二个是函数结尾,组合起来就是模块的包裹函数:

function (exports, require, module, __filename, __dirname) {
  // 模块内容写在这里
}

require函数的加载流程

当调用require函数引入一个模块时,会按照以下核心步骤执行:

  • 解析模块路径,确定模块的完整路径
  • 检查模块缓存,如果缓存中存在则直接返回缓存的module.exports
  • 如果缓存不存在,则加载模块内容,执行模块的包裹函数
  • 将执行后的module.exports存入缓存,并返回结果

路径解析规则

require函数的路径解析遵循一定的优先级:

路径类型解析规则
核心模块直接返回Node.js内置的核心模块,如fs、path等,优先级最高
以/、./、../开头的路径作为相对路径或绝对路径解析,查找对应文件,默认尝试.js、.json、.node后缀
非路径形式的名称从当前目录的node_modules开始向上查找,直到根目录的node_modules

模块的递归加载机制

递归加载是指当模块A依赖模块B,模块B又依赖模块C时,require函数会先加载模块C,再加载模块B,最后返回模块A的结果,这个过程是递归执行的。

我们可以通过三个模块来演示递归加载的过程:

模块c.js内容:

console.log('加载模块c');
exports.c = '我是模块c的内容';

模块b.js内容:

console.log('加载模块b');
// 引入模块c,触发递归加载
const c = require('./c');
exports.b = '我是模块b的内容,我引入了' + c.c;

模块a.js内容:

console.log('加载模块a');
// 引入模块b,模块b会先引入模块c
const b = require('./b');
console.log('模块a获取到的b内容:', b.b);

执行node a.js,输出结果如下:

加载模块a
加载模块b
加载模块c
模块a获取到的b内容: 我是模块b的内容,我引入了我是模块c的内容

可以看到加载顺序是a -> b -> c,c加载完成后返回结果给b,b加载完成后再返回结果给a,这就是递归加载的典型表现。

模块的缓存机制

CommonJS模块加载完成后,会将模块的module.exports存入缓存,后续再次require同一个模块时,直接返回缓存结果,不会重复执行模块代码。

缓存的存储位置是require.cache对象,键是模块的完整路径,值是模块对象。我们可以通过以下代码验证缓存的存在:

模块counter.js内容:

let count = 0;
// 每次加载时count加1,但是因为缓存,第二次加载不会执行这段代码
count++;
console.log('counter模块加载次数:', count);
exports.getCount = function() {
  return count;
};

测试文件test.js内容:

// 第一次引入counter模块
const counter1 = require('./counter');
console.log('第一次获取count:', counter1.getCount());

// 第二次引入同一个counter模块
const counter2 = require('./counter');
console.log('第二次获取count:', counter2.getCount());

// 检查两个引入的是否是同一个对象
console.log('两个引用是否相等:', counter1 === counter2);
// 查看缓存中是否存在counter模块
console.log('缓存中是否有counter模块:', !!require.cache[require.resolve('./counter')]);

执行node test.js的输出结果:

counter模块加载次数: 1
第一次获取count: 1
第二次获取count: 1
两个引用是否相等: true
缓存中是否有counter模块: true

可以看到counter模块只加载了一次,两次引入返回的是同一个对象,缓存生效。

缓存的注意事项

  • 缓存是基于模块的完整路径的,即使两个模块内容相同,路径不同也会被视为不同模块,分别缓存
  • 如果需要清除缓存,可以删除require.cache中对应的键,但不建议在生产环境使用
  • 循环引用时,缓存机制可以保证不会陷入无限递归,被引用但未加载完成的模块会返回未执行完成的exports对象

循环引用场景下的加载表现

当两个模块相互引用时,CommonJS的递归和缓存机制会如何处理?我们来看一个例子:

模块a.js内容:

console.log('开始加载a模块');
exports.a = 'a模块初始值';
// 引入b模块
const b = require('./b');
console.log('a模块中b模块的内容:', b.b);
exports.a = 'a模块最终值';

模块b.js内容:

console.log('开始加载b模块');
exports.b = 'b模块初始值';
// 引入a模块,形成循环引用
const a = require('./a');
console.log('b模块中a模块的内容:', a.a);
exports.b = 'b模块最终值';

执行node a.js的输出结果:

开始加载a模块
开始加载b模块
b模块中a模块的内容: a模块初始值
a模块中b模块的内容: b模块最终值

加载流程分析:

  1. 加载a模块,执行到引入b模块时,暂停a模块的加载,开始加载b模块
  2. 加载b模块,执行到引入a模块时,检查缓存发现a模块已经在加载中,直接返回当前a模块的exports(此时a的值还是初始值)
  3. b模块加载完成,将结果存入缓存,回到a模块继续执行
  4. a模块加载完成,更新exports的值,存入缓存

这就是循环引用下的加载逻辑,缓存机制避免了无限递归,同时返回的是加载到当前步骤的exports对象。

总结

CommonJS的require函数通过递归的方式处理模块依赖,从入口模块开始逐层加载依赖的模块,直到所有依赖都加载完成。同时模块缓存机制保证了同一个模块不会被重复加载,提升了模块加载的效率。理解这两个机制,能帮助开发者更好地处理模块依赖、循环引用等场景,避免因为模块加载逻辑不熟悉导致的bug。

CommonJSrequire模块加载模块缓存递归加载修改时间:2026-07-21 06:06:14

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。