面试题答案
一键面试Node.js模块加载流程
- 路径分析:根据传入
require
的参数,分析是核心模块、相对路径模块还是第三方模块。- 核心模块:Node.js内置模块,如
http
、fs
等,直接加载,加载速度最快。 - 相对路径模块:以
./
或../
开头,会根据当前模块的路径来确定实际路径。 - 第三方模块:从当前模块所在目录的
node_modules
开始查找,若找不到则向上级目录的node_modules
查找,直到根目录。
- 核心模块:Node.js内置模块,如
- 文件定位:确定模块路径后,会尝试查找对应的文件。先查找
.js
文件,若不存在则查找.json
文件,最后查找.node
文件(C++ 插件)。如果找到目录,则会查找目录下的package.json
文件,根据其中的main
字段确定入口文件,若没有main
字段则默认查找index.js
。 - 编译执行:找到文件后,根据文件类型进行编译执行。
.js
文件:通过fs
模块读取文件内容,然后使用vm
模块将其包装在一个函数中执行,该函数的参数为exports
、require
、module
、__filename
、__dirname
。.json
文件:通过fs
模块读取文件内容,然后使用JSON.parse
解析为JavaScript对象。.node
文件:通过process.dlopen
方法加载C++插件。
循环依赖处理
当出现循环依赖时,Node.js会返回一个已部分加载的模块对象。具体过程如下:
- 模块A加载模块B:模块A开始加载,在加载过程中遇到
require('B')
,于是开始加载模块B。 - 模块B加载模块A:模块B在加载过程中又遇到
require('A')
,此时模块A尚未完全加载完成,但Node.js会返回一个已部分加载的模块A对象给模块B,模块B继续加载并执行。 - 模块B加载完成:模块B加载完成后,控制权返回给模块A,模块A继续加载并执行剩余部分。
代码示例
- 模块定义阶段循环依赖示例
- a.js
console.log('a.js开始加载');
const b = require('./b');
console.log('a.js引用b模块后');
exports.a = 'a的值';
console.log('a.js加载完成');
- b.js
console.log('b.js开始加载');
const a = require('./a');
console.log('b.js引用a模块后');
exports.b = 'b的值';
console.log('b.js加载完成');
- main.js
console.log('main.js开始');
const a = require('./a');
console.log('main.js引用a模块后');
console.log('main.js结束');
- 运行结果
- 执行
node main.js
,输出:
- 执行
main.js开始
a.js开始加载
b.js开始加载
a.js引用b模块后
b.js引用a模块后(这里a模块是部分加载的,exports.a的值还未定义)
b.js加载完成
a.js加载完成
main.js引用a模块后
main.js结束
- 模块引用阶段循环依赖示例
- c.js
exports.c1 = 'c1初始值';
const d = require('./d');
exports.c2 = 'c2赋值';
console.log('c.js加载完成');
- d.js
const c = require('./c');
console.log(c.c1); // 这里可以获取到c1初始值
// console.log(c.c2); // 这里获取不到c2,因为c模块还未完全加载完成
exports.d = 'd的值';
console.log('d.js加载完成');
- main2.js
console.log('main2.js开始');
const c = require('./c');
console.log('main2.js引用c模块后');
console.log('main2.js结束');
- 运行结果
- 执行
node main2.js
,输出:
- 执行
main2.js开始
c.js加载完成
d.js加载完成
c1初始值
main2.js引用c模块后
main2.js结束
在这个示例中,模块d
在模块c
完全加载完成前引用了c
,可以获取到c
模块已定义的部分属性。