NodeJS里经常用到require,require的模块加载机制是什么
一、require模块是否需要import
用 Node.js 的 require
加载模块时,不需要用 import
。
这两者其实是两套不同的模块系统:
-
require
是 Node.js 传统的 CommonJS 模块加载方式。 -
import
是 ECMAScript 2015+(ES6)的 ES Modules 语法。
区别简要说明:
方式 | 语法形式 | 运行环境 | 支持情况 |
---|---|---|---|
require | const x = require('x') | Node.js 默认支持,所有版本 | CommonJS 模块,动态加载 |
import | import x from 'x' | 浏览器和 Node.js(需配置) | ES Modules,静态加载 |
在 Node.js 中:
-
如果用
.js
文件默认(CommonJS 模式),用require
加载,不能用import
(会报错,除非用实验特性或.mjs
文件)。 -
如果想用 ES Module,文件一般用
.mjs
后缀或者package.json
中设置"type": "module"
,这时用import
,不能用require
。
结论:
-
你只用
require
就够了,不需要import
。 -
这两者是两种不同的模块加载语法,不要混用。
二、Node.js 中 require
的完整模块加载过程(重点版)
-
检查缓存区(
require.cache
)-
先查用户加载的文件模块的缓存区(以文件绝对路径为key)。如果缓存命中,直接返回缓存内容。
-
注意:缓存只针对文件模块(JavaScript 文件、JSON 文件、.node 二进制模块)。
-
-
判断是否是核心模块(Core Module)
-
如果缓存没命中,接下来判断这个模块名是否是 Node.js 的核心模块(比如
fs
、http
、path
等)。 -
核心模块的加载是从 Node.js 内部直接提供的,不经过文件系统查找。
-
Node 有一个核心模块缓存(和
require.cache
不同),如果命中则直接返回核心模块的导出。 -
如果核心模块第一次加载,会加载并缓存核心模块实例。
-
-
模块路径解析
-
如果不是核心模块,进行路径解析,查找文件系统上的具体文件位置(支持相对路径、绝对路径、node_modules 目录等)。
-
-
读取并编译模块
-
读取文件内容后根据类型执行对应操作(JavaScript、JSON、native addon)。
-
-
缓存模块
-
编译执行完成后,将模块缓存到
require.cache
。
-
-
返回模块导出
核心模块缓存和文件模块缓存是分开的:
-
require.cache
只缓存文件模块的导出。 -
核心模块缓存由 Node.js 内部管理,用户不可见,但效果是类似的:只会加载一次。
如果想看源码,Node.js 的模块加载逻辑主要在 lib/internal/modules/cjs/loader.js 中,尤其是:
-
Module._load
实现了缓存判断和核心模块判断的逻辑。 -
NativeModule.get
用于加载核心模块。
总结:
require('module') 过程是:1. 查看文件模块缓存 require.cache
2. 若没命中,判断是否核心模块(内置模块)- 是则从内置模块缓存返回
3. 否则解析文件路径,加载文件模块
4. 编译执行,缓存
5. 返回模块导出
你之前代码的补充说明
const mod1 = require('./mod'); // 第一次加载,mod.js被执行,结果被缓存
const mod2 = require('./mod'); // 第二次加载,直接从缓存中拿,mod.js不会再执行
第1步就是判断缓存,这样避免重复加载,提升性能。
如果你想,我可以帮你写个伪代码模拟这整个流程,方便你理解。需要吗?
三、require的模块加载机制
关于 Node.js 里的 require
,它是用来加载模块的,背后有一套比较复杂的模块加载机制。下面我详细解释一下 require
的模块加载机制。
Node.js require
模块加载机制详解
1. 作用
require
用来加载和使用模块,它可以加载:
-
核心模块(如
fs
、path
) -
文件模块(本地文件,JS、JSON、Node 原生支持的其他文件)
-
第三方模块(
node_modules
里的包)
2. 加载流程
当执行 require('some-module')
时,Node 会经历如下步骤来找到并加载模块:
2.1 判断模块类型
-
核心模块(Core Modules)
例如fs
、http
、path
等,如果是核心模块,直接返回核心模块导出的对象,不访问文件系统。 -
文件路径
如果参数是相对路径或绝对路径(./foo
、../bar
、/abs/path
),则加载指定路径的文件。 -
第三方模块
如果不是核心模块也不是路径,Node 会从当前目录开始向上查找node_modules
目录,直到根目录。
2.2 路径解析(文件模块 & 第三方模块)
-
如果传入的是文件路径(相对或绝对),Node 会按照下面顺序查找文件:
-
直接看路径对应的文件是否存在,比如
./foo.js
-
如果路径是文件夹,Node 会尝试加载文件夹下的
package.json
里"main"
字段指向的入口文件 -
如果没找到
package.json
或"main"
,尝试加载文件夹下的index.js
或index.json
等默认文件
-
-
对第三方模块,Node 会依次向上查找最近的
node_modules
文件夹里的对应模块文件夹。
2.3 模块缓存
-
缓存机制:当一个模块第一次被加载后,会被缓存,后续
require
同一模块时直接返回缓存,避免重复执行,提高效率。 -
缓存存储在
require.cache
对象里,键是模块的绝对路径。
2.4 模块包装和执行
-
Node 会给每个模块包装一层函数,形成闭包。模块代码被包裹成如下形式:
(function (exports, require, module, __filename, __dirname) {// 模块代码在这里执行
});
-
这样模块内部就可以使用
exports
导出接口,require
加载其他模块,module
表示当前模块等。 -
模块执行时,Node 会传入这些参数,让模块成为一个私有作用域。
2.5 模块导出机制
-
模块通过
exports
或module.exports
导出内容。 -
exports
是module.exports
的引用,直接修改exports
不改变导出对象,赋值新对象要用module.exports = ...
3. 额外说明
-
.js
文件被视作 JavaScript 脚本执行。 -
.json
文件会被自动解析成对象。 -
.node
文件是编译后的二进制 addon,会被加载执行。
总结
-
判断模块类型(核心模块,文件模块,第三方模块)
-
解析模块路径(寻找文件或目录)
-
加载模块(读取代码,封装成函数)
-
执行模块代码(提供闭包参数)
-
缓存模块导出结果
-
返回模块导出
如果你需要,我可以帮你写一个示例,演示 require
的缓存或者自定义模块加载过程,或者给你推荐一些原理文档。你需要吗?
四、require
的缓存机制演示
我先给你演示一下 Node.js 中 require
的缓存机制和自定义模块加载过程,然后给你推荐一些深入理解 Node.js 模块加载机制的官方文档和源码相关资料。
1. require
的缓存机制演示
Node.js 中,当你用 require('module')
加载模块时,模块只会被加载执行一次,后续再 require
相同模块,直接从缓存拿,不会重新执行。
// mod.js
console.log('mod.js 执行了');
module.exports = { count: 0 };// main.js
const mod1 = require('./mod');
mod1.count++;
console.log('第一次载入:', mod1.count);const mod2 = require('./mod');
console.log('第二次载入:', mod2.count);// 输出:
// mod.js 执行了
// 第一次载入: 1
// 第二次载入: 1
解释:
-
第一次
require('./mod')
,文件执行,导出对象缓存起来。 -
第二次
require('./mod')
,直接从缓存拿,没有再执行mod.js
文件。 -
mod1
和mod2
是同一个对象,count
的状态被共享。
2. 自定义模块加载过程简单示例
你可以在 Node.js 中自定义如何加载模块,利用 Module._load
或者实现自定义的 require
函数(常用场景是修改模块行为或加载非 .js
文件)。
以下示例是简单拦截 require
加载过程打印模块名:
const Module = require('module');
const originalLoad = Module._load;Module._load = function(request, parent, isMain) {console.log(`加载模块: ${request}`);return originalLoad.apply(this, arguments);
};const fs = require('fs'); // 会打印:加载模块: fs
const path = require('path'); // 会打印:加载模块: path
const mymod = require('./mod'); // 会打印:加载模块: ./mod
这个示例中,改写了内部的 _load
方法,实现了加载日志。
3. 推荐学习资源和官方文档
-
Node.js 模块官方文档(英文)
详尽解释 CommonJS 模块加载流程,缓存机制,加载路径解析等。 -
Node.js 源码中的 Module 模块
Node.js 模块加载器核心源码,理解底层机制非常关键。 -
《深入浅出Node.js》(朴灵)
书中对模块机制有详细介绍,通俗易懂。 -
Module._load 源码注释文章(中文)
分析了_load
方法的实现原理。
如果你需要,我还可以帮你写一个更复杂的自定义 require 例子(比如加载 JSON、文本文件或者给模块加缓存时间限制之类的)。你想看吗?