Skip to content

Node.js require 实现原理详解与简易实现

1. require 本质

在 Node.js 中,require 实际上是 Module.prototype.require 的一个别名。每当你 require('foo'),底层就是调用的 Module.prototype.require


2. 加载流程概述

  1. 路径解析

    • 调用 path.resolve(__dirname, <路径>) 得到唯一的绝对路径。
    • 判断该文件是否存在,若不存在则尝试自动补全常见后缀(如 .js, .json)。
    • 最终确定一个可用的绝对路径。
  2. 缓存处理

    • Node.js 内部维护一个缓存对象,第一次加载某模块时会将其实例缓存,后续直接返回缓存,避免重复加载和死循环。
  3. Module 实例化

    • new Module(<绝对路径>) 创建模块对象。
    • 构造器中 this.id = id 保存绝对路径,this.exports = {} 保存该模块导出的内容。
  4. 读取与编译文件

    • 不同的文件类型采取不同的加载策略。
      • .js 文件会包裹为一个函数(接收 5 个参数:exports, require, module, __filename, __dirname),并用 vm.runInThisContext 编译执行。
      • .json 文件直接读内容并用 module.exports = JSON.parse(content)
    • 执行模块代码后,module.exports 上的内容就是该模块真正导出的内容。
  5. 返回导出对象

    • 最终返回 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.json

4. 关键点优化与补充

  • 模块缓存:每个模块只会被加载一次,后续引用直接返回缓存,防止死循环和性能浪费。
  • 路径补全:自动尝试.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.exports

foo 模块的 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'
]