Node.js 遵循 CommonJS 规范时模块的加载机制
- 首次加载:当 Node.js 遇到
require
语句加载模块时,它会首先检查该模块是否已经在缓存中。如果模块在缓存中,则直接返回缓存中的导出对象。
- 模块定位:若模块不在缓存中,Node.js 会根据
require
中的路径来定位模块。路径可以是核心模块(如 fs
、http
等),直接加载;也可以是相对路径(如 ./module.js
)或绝对路径,按路径查找;还可以是包名,在 node_modules
目录中查找。
- 模块编译:找到模块后,Node.js 将模块代码包装在一个函数中,该函数提供了
exports
、require
、module
等局部变量。然后对包装后的代码进行编译执行。执行过程中,模块通过 exports
或 module.exports
来暴露其接口。
循环依赖处理
- 处理方式:当出现循环依赖时,Node.js 会在模块第一次被
require
时,就为其在缓存中创建一个空对象。然后继续加载依赖的模块。当依赖模块加载完成并填充其 exports
对象后,再回过头来完成最初模块的加载。
- 举例说明:
// a.js
console.log('a starting');
const b = require('./b');
console.log('a b:', b.value);
exports.value = 'a';
console.log('a finished');
- **模块 B(b.js)**:
// b.js
console.log('b starting');
const a = require('./a');
console.log('b a:', a.value);
exports.value = 'b';
console.log('b finished');
- **主模块(main.js)**:
// main.js
console.log('main starting');
const a = require('./a');
console.log('main a:', a.value);
console.log('main finished');
- **分析处理过程**:
- 在 `main.js` 中 `require('./a')`,Node.js 开始加载 `a.js`。
- `a.js` 开始执行,打印 `a starting`,然后 `require('./b')`,开始加载 `b.js`。
- `b.js` 开始执行,打印 `b starting`,接着 `require('./a')`。此时 `a.js` 正在加载中,Node.js 从缓存中取出 `a.js` 对应的空对象返回给 `b.js`。
- `b.js` 继续执行,打印 `b a: undefined`,因为此时 `a.js` 还未完成加载,`a.value` 未定义。然后 `b.js` 给 `exports.value` 赋值为 `b`,打印 `b finished`,`b.js` 加载完成。
- 回到 `a.js`,继续执行,此时 `b.value` 已定义,打印 `a b: b`,然后给 `exports.value` 赋值为 `a`,打印 `a finished`,`a.js` 加载完成。
- 回到 `main.js`,继续执行,打印 `main a: a`,然后打印 `main finished`。最终结果展示了 Node.js 对循环依赖的处理过程,虽然有循环依赖,但仍能正确加载并执行各模块。