CommonJS require机制递归逻辑及wrapper函数原理咨询
关于这个
require实现的递归机制与wrapper函数解析 递归加载的工作逻辑
这个require的递归机制靠自身调用+缓存机制实现,核心流程如下:
- 第一次调用
require('moduleA')时,先检查require.cache里是否存在该模块——如果没有,就读取模块代码,创建带exports属性的空module对象,立刻把这个module存入缓存(这一步是避免循环依赖导致无限递归的关键)。 - 接着把模块代码包装成
wrapper函数并执行,在执行模块A代码的过程中,如果里面出现require('moduleB'),会直接递归调用同一个require函数。 - 进入
require('moduleB')的流程后,重复「检查缓存-读取代码-存缓存-执行」的逻辑,直到模块B及其依赖的所有模块都加载完成,再回到模块A的代码继续执行。 - 所有依赖处理完毕后,
require返回模块A的exports对象。
缓存的作用不止是避免重复加载,还能解决循环依赖:比如模块A依赖B、模块B又依赖A,加载B时调用require('A'),此时A已经被存入缓存(哪怕A的代码还没执行完),会直接返回A当前的exports,不会重新加载A,从根源避免无限递归。
为什么需要wrapper函数?
这个wrapper是模块系统的核心设计,解决了两个核心问题:
- 隔离模块作用域:每个模块的代码都在
wrapper的局部作用域内执行,模块内部的变量、函数都是局部的,既不会污染全局作用域,也不会和其他模块的变量产生冲突。 - 注入核心变量:用
Function构造函数创建wrapper时,指定了require、exports、module三个参数,执行wrapper时会把外部的require函数、当前模块的exports和module对象传进去。这样模块代码里就能直接使用这三个变量——比如写require('./B')就是调用顶层的require函数实现递归加载;写exports.foo = ...就是直接修改当前模块的exports对象。
举个实际例子,假设模块A的代码是:
const B = require('./B'); exports.run = () => B.print();
当wrapper执行时,这段代码就等价于在一个函数内部运行:
function(require, exports, module) { const B = require('./B'); exports.run = () => B.print(); }
这里的require就是我们定义的全局require函数,调用它就能触发递归加载模块B,整个依赖加载流程就串起来了。
内容的提问来源于stack exchange,提问作者Yousef
相关产品推荐
相关产品推荐

