Appearance
Node.js require 实现原理详解与简易实现
1. require 本质
在 Node.js 中,require 实际上是 Module.prototype.require 的一个别名。每当你 require('foo'),底层就是调用的 Module.prototype.require。
2. 加载流程概述
路径解析
- 调用
path.resolve(__dirname, <路径>)得到唯一的绝对路径。 - 判断该文件是否存在,若不存在则尝试自动补全常见后缀(如
.js,.json)。 - 最终确定一个可用的绝对路径。
- 调用
缓存处理
- Node.js 内部维护一个缓存对象,第一次加载某模块时会将其实例缓存,后续直接返回缓存,避免重复加载和死循环。
Module 实例化
new Module(<绝对路径>)创建模块对象。- 构造器中
this.id = id保存绝对路径,this.exports = {}保存该模块导出的内容。
读取与编译文件
- 不同的文件类型采取不同的加载策略。
.js文件会包裹为一个函数(接收 5 个参数:exports,require,module,__filename,__dirname),并用vm.runInThisContext编译执行。.json文件直接读内容并用module.exports = JSON.parse(content)。
- 执行模块代码后,
module.exports上的内容就是该模块真正导出的内容。
- 不同的文件类型采取不同的加载策略。
返回导出对象
- 最终返回
module.exports。
- 最终返回
3. 源码级简易实现
js
const vm = require('vm')
const fs = require('fs')
const path = require('path')
class Module {
constructor(id) {
this.id = id // 文件的绝对路径
this.exports = {}
}
static _cache = {}
static _extensions = {
'.js'(module, filename) {
const content = fs.readFileSync(filename, 'utf-8')
module._compile(content, filename)
},
'.json'(module, filename) {
const content = fs.readFileSync(filename, 'utf-8')
module.exports = JSON.parse(content)
}
}
static _resolveFilename(request) {
let filePath = path.resolve(__dirname, request)
if (fs.existsSync(filePath)) return filePath
// 补全后缀
for (const ext of Object.keys(this._extensions)) {
const tryPath = filePath + ext
if (fs.existsSync(tryPath)) return tryPath
}
throw new Error(`Cannot find module '${request}'`)
}
static _load(request) {
const filename = Module._resolveFilename(request)
const cache = Module._cache
// 缓存命中
if (cache[filename]) {
return cache[filename].exports
}
const module = new Module(filename)
cache[filename] = module
module.load(filename)
return module.exports
}
require(id) {
return Module._load(id)
}
load(filename) {
const ext = path.extname(filename)
const handler = Module._extensions[ext]
if (!handler) throw new Error(`Unknown extension: ${ext}`)
handler(this, filename)
}
_compile(content, filename) {
const dirname = path.dirname(filename)
const wrapper = `(function(exports, require, module, __filename, __dirname) {
${content}
})`
const compiledFn = vm.runInThisContext(wrapper, { filename })
compiledFn.call(this.exports, this.exports, this.require.bind(this), this, filename, dirname)
}
}
// 对外暴露的 require
function req(requestPath) {
return Module.prototype.require.call({ require: Module.prototype.require }, requestPath)
}
// 使用示例
console.log(req('./foo')) // 加载 foo.js 或 foo.json4. 关键点优化与补充
- 模块缓存:每个模块只会被加载一次,后续引用直接返回缓存,防止死循环和性能浪费。
- 路径补全:自动尝试
.js、.json等常见后缀,兼容常见写法。 - 文件类型扩展:通过
_extensions支持不同类型文件的加载逻辑,方便扩展。 - 安全沙箱:通过
vm.runInThisContext,在沙箱环境下执行模块代码,保证隔离。 - 执行上下文:为每个模块提供独立的
exports,require,module,__filename,__dirname,模拟真实 Node.js 环境。 - 异常处理:找不到文件或后缀不支持时抛出明确错误。
5. 与真实 Node.js 区别
- Node.js 真实实现更加复杂,支持 ES Module、C/C++ 扩展、异步加载等。
- 真实的 require 还支持查找
node_modules目录、包的main字段等。 - 真实的缓存机制和循环依赖处理更为完善。
- 真实 Node.js 支持更多文件类型(如
.node),并有更严格的错误兼容和性能优化。
6. 总结
- Node.js 的
require本质是对Module类的封装。 - 路径解析、缓存、不同类型文件的加载、沙箱执行、参数注入等是核心思想。
- 理解
require的原理有助于深入理解 Node.js 模块系统、调试和自定义模块加载机制。
exports 和 module.exports 的关系?
exports 是 module.exports 的别名,它们指向同一个地址,exports 只是为了赋值属性时 方便。
js
const exports = module.exportsfoo 模块的 global 和 bar 模块的 global 是一样的吗?
所有相关模块的 global 都是同一个,尽量不往 global 上添加属性,可能会污染全局,除非 这个东西很多地方都要用到。
结果是 bar,因为返回的是 module.exports。
js
exports.foo = 'foo'
module.exports = 'bar'
js
// foo.js
const o = {
foo: 'foo'
}
setTimeout(() => {
o.foo = 'bar'
}, 1000)
module.exports = o
// bar.js
const foo = require('./foo') // { foo: 'foo' }
setTimeout(() => {
console.log(foo) // { foo: 'bar' }
}, 2000)
js
// foo.js
let foo = 'foo'
setTimeout(() => {
foo = 'bar'
}, 1000)
module.exports = foo
// bar.js
const foo = require('./foo') // 'foo'
setTimeout(() => {
console.log(foo) // 'foo'
const res = require('./foo')
console.log(res) // 'foo',走缓存,这里缓存的值是基本数据类型。
}, 2000)文件模块的解析流程
默认先找文件(没找到会尝试添加后缀继续查找),再找文件夹,如果找到文件后就不再找文件夹。 老版本会先找 package.json 中的 main 入口,找 main 指定的文件。 如果没有 ./ 或 ../ 或 绝对路径,会认为此模式是第一个第三方模块或核心模块。
js
const foo = require('./foo')第三方模块的解析流程
第三方模块分为全局和本地,本地的第三方模块安装在 node_modules 中,如果没找到, 沿着 module.paths 查找。全局的第三方只能在任何目录下通过命令行使用。
js
[
'C:\\Users\\xNoRain\\Desktop\\node\\node_modules',
'C:\\Users\\xNoRain\\Desktop\\node_modules',
'C:\\Users\\xNoRain\\node_modules',
'C:\\Users\\node_modules',
'C:\\node_modules'
]