CommonJS
在 Node.js 模块系统中,每个文件都被视为独立的模块。
基础使用
module.exports导出require()加载
Module 实例
Node 内部提供了一个构建函数 module ,而每个模块都是一个 module 实例
// module实例对象
Module {
id: '.',
path: '/Users/donggua/learning/daily',
exports: {},
parent: null,
filename: '/Users/donggua/learning/daily/mod.js',
loaded: false,
children: [],
paths: [
'/Users/donggua/learning/daily/node_modules',
'/Users/donggua/learning/node_modules',
'/Users/donggua/node_modules',
'/Users/node_modules',
'/node_modules'
]
}module.id标识符,'.' 或者是带有绝对路径的模块文件名module.filename文件名,带有绝对路径module.loaded表示模块是否已完成加载
当前例子是单独的模块,没有被 require 调用使用,所以为 false
module.parent表示调用该模块的模块
当前例子没有被调用,所以返回值是 null
Note
于 v14.6.0, v12.19.0 版本已弃用,使用 require.main 代替
module.children表示模块所调用的其他模块module.exports表示模块对外输出的值,默认{}module.path模块所在目录名称module.paths模块的搜索路径
exports 变量
Node 为每个模块提供一个 exports 变量,指向 module.exports ,因此模块导出有以下几种写法
module.exports.a = 1
// 等同于
module.exports = { a: 1 }
// 或
exports.a = 1同时应该注意, 我们不能修改 exports 的指向
// 以下写法是错误且无效的
exports = 'angthing'模块的缓存机制
当执行 require(path) 时,会先使用 path 为 id 从模块的缓存 Module._cache 中检查,存在则直接从缓存中读取返回对应的 module.exports ,因此多次调用 require(path) 不会导致模块代码被多次执行
require('./a')
require('./a').done = true
require('./a').done // trueNode 中使用 require.cache 指向 Module._cache 给予开发者访问查看模块的缓存 若需要删除对应的模块缓存,可以使用 delete require.cache[path]
循环加载
/// 这里直接引用官方demo
// a.js
console.log('a starting')
exports.done = false
const b = require('./b.js')
console.log('in a, b.done = %j', b.done)
exports.done = true
console.log('a done')
// b.js
console.log('b starting')
exports.done = false
const a = require('./a.js')
console.log('in b, a.done = %j', a.done)
exports.done = true
console.log('b done')
// main.js
console.log('main starting')
const a = require('./a.js')
const b = require('./b.js')
console.log('in main, a.done = %j, b.done = %j', a.done, b.done)为了防止无限循环,在循环引用时, Node 会返回未完成的不完整副本。
require 加载机制
源码分析
CommonJs 源码位于 node/lib/internal/modules/cjs ,这里主要看 loaders.js
// 模块构造函数
function Module(id = '', parent) {
this.id = id
this.path = path.dirname(id)
this.exports = {}
moduleParentCache.set(this, parent)
updateChildren(parent, this, false)
this.filename = null
this.loaded = false
this.children = []
}
// 以通过的文件路径为id,加载并返回模块的exports属性
Module.prototype.require = function (id) {
validateString(id, 'id')
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string')
}
requireDepth++
try {
return Module._load(id, this, /* isMain */ false)
} finally {
requireDepth--
}
}注
以下源码省略了原生模块 NativeModule等部分无关本次分析的代码
// 1. 如果缓存中有模块数据,则从缓存中取出并返回模块的exports属性
// 2. 通过构造方法生成新的模块,保存到缓存中并返回模块的exports属性
Module._load = function (request, parent, isMain) {
let relResolveCacheIdentifier
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id)
// Fast path for (lazy loaded) modules in the same directory. The indirect
// caching is required to allow cache invalidation without changing the old
// cache key names.
relResolveCacheIdentifier = `${parent.path}\x00${request}`
const filename = relativeResolveCache[relResolveCacheIdentifier]
if (filename !== undefined) {
const cachedModule = Module._cache[filename]
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true)
if (!cachedModule.loaded) return getExportsForCircularRequire(cachedModule)
return cachedModule.exports
}
delete relativeResolveCache[relResolveCacheIdentifier]
}
}
//...
// 检查缓存
const cachedModule = Module._cache[filename]
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true)
if (!cachedModule.loaded) {
const parseCachedModule = cjsParseCache.get(cachedModule)
if (!parseCachedModule || parseCachedModule.loaded)
return getExportsForCircularRequire(cachedModule)
parseCachedModule.loaded = true
} else {
return cachedModule.exports
}
}
// 从模块映射中查看是否有符合条件的
const mod = loadNativeModule(filename, request)
if (mod?.canBeRequiredByUsers) return mod.exports
// Don't call updateChildren(), Module constructor already does.
const module = cachedModule || new Module(filename, parent)
if (isMain) {
process.mainModule = module
module.id = '.'
}
Module._cache[filename] = module
//...
return module.exports
}- 当执行
require(path)时,node才会去执行对应的模块 - 通过
Module._load()函数解析并返回module.exports对象,require()实际就是加载了exports对象 - 以
path作为id进行缓存,对同一模块require()加载将直接从缓存中获取并返回
其中,模块的缓存在 helper.js 中通过 require.cache 指向 Module._cache
实例 debugger 验证
debugger 流程:
- 执行
node --inspect-brk=9229 main命令 - 在chrome浏览器中打开
chrome://inspect并点击Remote Target中的main(文件名)标识的inspect
--inspect-brk用于首行开始 debugger,否则用--inspect。9229为指定端口号
// a.js
let done = false
function change() {
done = false
console.log('in a, change inside', done)
}
function checkIsChange() {
console.log('in a, real done', done)
}
module.exports = { done, change, checkIsChange }// b.js
const a = require('./a')
console.log('in b, before change', a.done)
a.done = true
console.log('in b, done changed', a.done)
a.checkIsChange()
a.change()
module.exports = {}// main.js
debugger
const a = require('./a')
const b = require('./b')
console.log('in main, a.done', a.done)# 输出结果
in b, before change false
in b, done changed true
in a, real done false
in a, change inside false
in main, a.done true同时我们会发现:
b.js中的a.done = true只影响了main.js中的输出结果b.js中调用a.change()方法没有影响到main.js和b.js的输出结果
通过debugger可以看到:
当 main.js 运行到 const a = require("./a"); 时, node 才执行 Module._load() 获取模块 a

而 b.js 再次加载模块 a 时,是通过缓存机制读取的。
总结
CommonJs属于运行时加载CommonJs是同步加载require()是对exports对象的赋值操作,而Module._load()借用函数形式保证模块内外不会互相影响