手写实现一个简化的 CommonJS 模块加载器:理解 require 的同步、缓存与导出机制

CommonJS 模块加载器:深入理解 require 的同步、缓存与导出机制

各位技术同仁,欢迎来到今天的技术讲座。我们将深入探讨 CommonJS 模块系统的核心机制,并通过手写实现一个简化的模块加载器来揭示 require 函数背后的秘密。理解 require 的同步加载特性、模块缓存机制以及灵活的导出方式,不仅能帮助我们更好地编写 Node.js 应用,更是理解现代 JavaScript 模块化演进历程的关键一步。

CommonJS 是 Node.js 早期采用的模块化规范,它通过 require 语句导入模块,通过 module.exportsexports 导出模块。其设计理念简洁而强大,尤其适用于服务器端同步加载的场景。与浏览器端的异步加载(如 AMD)形成鲜明对比,CommonJS 模块在被 require 时会立即执行,并返回其导出的内容。

1. CommonJS 模块化的核心概念

在 Node.js 环境中,每个文件都被视为一个独立的模块。模块内部的代码默认私有,不会污染全局作用域。这种隔离性是通过一个特殊的“模块包装器”实现的。

当 Node.js 加载一个模块文件时,它实际上会将其内容包裹在一个函数表达式中,这个函数在执行时会接收到几个重要的参数:

  • exports: 一个对象,用于导出模块的公共接口。
  • require: 一个函数,用于加载其他模块。
  • module: 一个对象,代表当前模块,module.exports 是其最重要的属性。
  • __filename: 当前模块文件的绝对路径。
  • __dirname: 当前模块文件所在目录的绝对路径。

这个包装器函数的骨架大致如下:

(function(exports, require, module, __filename, __dirname) {
  // 模块的实际代码内容
  // 例如:
  // const foo = require('./foo');
  // module.exports = { ... };
})(exports, require, module, __filename, __dirname);

理解这个包装器是理解 CommonJS 模块加载机制的起点。它为每个模块提供了一个私有的作用域,并注入了必要的工具函数和对象。

2. 模块加载器的基本架构

要实现一个简化的 CommonJS 模块加载器,我们需要解决以下几个关键问题:

  1. 文件读取: 如何从文件系统读取模块的源代码?
  2. 路径解析: 如何将 require 的路径参数解析为实际的文件路径?
  3. 模块缓存: 如何确保模块只被加载和执行一次?
  4. 模块执行: 如何在一个隔离的环境中执行模块代码,并捕获其导出内容?
  5. 导出机制: 如何处理 exportsmodule.exports

我们将围绕一个 Module 类和核心的 myRequire 函数来构建我们的加载器。

2.1. 模块状态与缓存机制

模块缓存是 CommonJS 模块加载器不可或缺的一部分。它保证了:

  • 性能优化: 避免重复读取文件和执行代码。
  • 一致性: 每次 require 同一个模块,都返回同一个实例。
  • 循环依赖处理: 帮助解决模块间的循环依赖问题。

我们将使用一个全局对象(或 Module 类的静态属性)作为缓存,以模块的绝对路径为键,存储已加载的 Module 实例。

2.2. Module 类的定义

首先,我们定义一个 Module 类,它将封装模块的各种状态和行为。

const path = require('path');
const fs = require('fs');
const vm = require('vm'); // 用于在沙箱中运行代码

class Module {
  constructor(id, parent) {
    this.id = id; // 模块的唯一标识符,通常是其绝对路径
    this.filename = null; // 模块文件的绝对路径
    this.parent = parent; // 引用加载此模块的父模块
    this.exports = {}; // 模块的导出对象
    this.loaded = false; // 标识模块是否已加载完成
    this.children = []; // 此模块加载的所有子模块
  }

  // 静态属性,用于存储所有已加载的模块缓存
  static _cache = {};

  // 静态属性,用于处理不同文件扩展名的加载逻辑
  static _extensions = {
    '.js': (module, filename) => {
      // 读取文件内容
      const content = fs.readFileSync(filename, 'utf8');
      // 编译并执行模块内容
      module._compile(content, filename);
    },
    '.json': (module, filename) => {
      // 读取 JSON 文件内容
      const content = fs.readFileSync(filename, 'utf8');
      // 直接解析为 JavaScript 对象并赋值给 module.exports
      try {
        module.exports = JSON.parse(content);
      } catch (err) {
        throw new Error(`Cannot parse JSON module ${filename}: ${err.message}`);
      }
    },
    // 更多扩展名,如 .node (C++ addon) 等,此处简化不实现
  };

  // 静态方法,用于解析模块的绝对路径
  static _resolveFilename(request, parentModule) {
    // 1. 处理绝对路径
    if (path.isAbsolute(request)) {
      if (fs.existsSync(request)) {
        return request;
      }
    }

    // 2. 处理相对路径
    const parentDir = parentModule ? path.dirname(parentModule.filename) : process.cwd();
    let resolvedPath = path.resolve(parentDir, request);

    // 尝试添加文件扩展名
    const extensions = Object.keys(Module._extensions);
    for (const ext of extensions) {
      if (fs.existsSync(resolvedPath + ext)) {
        return resolvedPath + ext;
      }
    }

    // 尝试作为目录加载
    if (fs.existsSync(resolvedPath)) {
      if (fs.statSync(resolvedPath).isDirectory()) {
        // 尝试加载 package.json 的 main 字段
        const packageJsonPath = path.join(resolvedPath, 'package.json');
        if (fs.existsSync(packageJsonPath)) {
          try {
            const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
            if (packageJson.main) {
              const mainPath = path.resolve(resolvedPath, packageJson.main);
              // 再次尝试各种扩展名
              for (const ext of extensions) {
                if (fs.existsSync(mainPath + ext)) {
                  return mainPath + ext;
                }
              }
              if (fs.existsSync(mainPath)) { // 如果 mainPath 本身就是一个文件
                  return mainPath;
              }
            }
          } catch (e) {
            // 忽略解析 package.json 的错误
          }
        }
        // 如果 package.json 没有指定 main,或解析失败,尝试加载 index.js
        if (fs.existsSync(path.join(resolvedPath, 'index.js'))) {
          return path.join(resolvedPath, 'index.js');
        }
      } else { // 如果 resolvedPath 存在但不是目录,也不是带扩展名的文件,则直接返回
        return resolvedPath;
      }
    }

    // 3. 处理 npm 模块 (这里简化,只在当前目录的 node_modules 中查找)
    // 实际的 Node.js 会向上遍历 node_modules 目录
    const nodeModulesPath = path.join(process.cwd(), 'node_modules', request);
    for (const ext of extensions) { // 尝试直接 require 'my-utility.js'
        if (fs.existsSync(nodeModulesPath + ext)) {
            return nodeModulesPath + ext;
        }
    }

    // 尝试作为 npm 模块目录加载
    if (fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory()) {
        const packageJsonPath = path.join(nodeModulesPath, 'package.json');
        if (fs.existsSync(packageJsonPath)) {
            try {
                const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
                if (packageJson.main) {
                    const mainPath = path.resolve(nodeModulesPath, packageJson.main);
                    for (const ext of extensions) {
                        if (fs.existsSync(mainPath + ext)) {
                            return mainPath + ext;
                        }
                    }
                    if (fs.existsSync(mainPath)) {
                        return mainPath;
                    }
                }
            } catch (e) {
                // 忽略解析 package.json 的错误
            }
        }
        // 尝试加载 index.js
        if (fs.existsSync(path.join(nodeModulesPath, 'index.js'))) {
            return path.join(nodeModulesPath, 'index.js');
        }
    }

    // 如果以上都找不到,抛出模块未找到错误
    const error = new Error(`Cannot find module '${request}' from '${parentModule ? parentModule.filename : process.cwd()}'`);
    error.code = 'MODULE_NOT_FOUND';
    throw error;
  }

  // 加载模块的核心方法
  load(filename) {
    this.filename = filename;
    this.id = filename; // 模块ID也设置为绝对路径

    // 获取文件扩展名
    const ext = path.extname(filename);
    // 根据扩展名调用对应的加载器
    if (Module._extensions[ext]) {
      Module._extensions[ext](this, filename);
    } else {
      // 默认按 .js 处理 (如果文件没有扩展名,Node.js 也会尝试 .js, .json, .node)
      // 在我们的简化实现中,如果_resolveFilename没有找到带扩展名的文件,这里可能已经报错了
      // 如果需要更严格模拟,可以在这里再次尝试 .js
      Module._extensions['.js'](this, filename);
    }

    this.loaded = true; // 标记模块已加载
  }

  // 编译并执行模块代码
  _compile(content, filename) {
    // 准备模块包装器的参数
    const dirname = path.dirname(filename);
    const requireFunc = this.createRequireFunction(); // 创建一个绑定到当前模块的 require 函数
    const exports = this.exports; // 初始的 exports 对象
    const module = this; // 当前模块对象

    // 构建模块包装器函数字符串
    const wrapper = `(function(exports, require, module, __filename, __dirname) {n${content}n});`;

    // 使用 vm 模块在沙箱中执行代码
    // 这里我们使用 vm.runInThisContext,这意味着模块代码将在当前 V8 上下文中执行
    // 但通过闭包和传递参数,我们仍然实现了作用域隔离和变量注入。
    // 更严格的沙箱会使用 vm.runInNewContext
    const compiledWrapper = vm.runInThisContext(wrapper, {
      filename: filename,
      lineOffset: 0,
      displayErrors: true,
    });

    // 调用包装器函数,传入参数
    compiledWrapper.call(exports, exports, requireFunc, module, filename, dirname);
  }

  // 为当前模块创建 require 函数实例
  createRequireFunction() {
    const self = this; // 捕获当前模块实例

    function localRequire(request) {
      // 解析请求路径
      const resolvedFilename = Module._resolveFilename(request, self);

      // 检查缓存
      if (Module._cache[resolvedFilename]) {
        return Module._cache[resolvedFilename].exports;
      }

      // 创建新的模块实例
      const module = new Module(resolvedFilename, self);
      Module._cache[resolvedFilename] = module; // 立即放入缓存,以处理循环依赖

      // 加载模块
      module.load(resolvedFilename);

      // 将新模块添加到父模块的子模块列表中
      self.children.push(module);

      // 返回模块的导出对象
      return module.exports;
    }

    // 可以在 require 函数上添加一些辅助方法,例如 Node.js 的 require.resolve
    localRequire.resolve = (request) => Module._resolveFilename(request, self);

    return localRequire;
  }
}

// 模拟 Node.js 的全局 require 函数,但我们自己实现
function myRequire(request) {
  // 顶层 require 没有父模块
  const resolvedFilename = Module._resolveFilename(request, null);

  // 检查缓存
  if (Module._cache[resolvedFilename]) {
    return Module._cache[resolvedFilename].exports;
  }

  // 创建一个“主”模块实例,作为所有模块的根
  // 在 Node.js 中,这个通常是入口文件 (process.argv[1])
  // 我们这里简化为直接加载请求的模块作为主模块
  const mainModule = new Module(resolvedFilename, null); 
  Module._cache[resolvedFilename] = mainModule; // 立即放入缓存

  // 加载模块
  mainModule.load(resolvedFilename);

  return mainModule.exports;
}

// 导出 Module 类和 myRequire 函数,以便 main.js 可以使用
module.exports = {
  Module,
  myRequire
};

2.3. 核心 myRequire 函数

现在,我们可以定义一个顶层的 myRequire 函数,作为我们模块加载器的入口。

// my_loader.js 文件中的 myRequire 函数实现
// ... (上面定义的 Module 类和 myRequire 函数代码) ...

3. 深入理解 CommonJS 的关键机制

3.1. 同步加载(Synchronous Loading)

我们的 myRequire 实现天然地体现了 CommonJS 的同步加载特性。观察 Module.prototype.load 方法:

  • fs.readFileSync(filename, 'utf8'):这是一个同步的文件读取操作,它会阻塞当前线程,直到文件内容完全读取完毕。
  • module._compile(content, filename):一旦内容读取完毕,模块代码会被立即编译和执行。

这意味着当你在一个模块中调用 require('./anotherModule') 时,程序会暂停执行当前模块,转而去加载、编译并执行 anotherModule。只有当 anotherModule 完全加载并返回其 exports 后,当前模块才会继续执行。

这种同步模型在服务器端(如 Node.js)非常有效,因为文件 I/O 通常发生在本地磁盘,延迟较低。但在浏览器环境中,同步加载会阻塞 UI 线程,导致页面假死,因此不适用。这也是为什么浏览器端普遍采用异步模块加载(如 ES Modules 或 AMD)的原因。

3.2. 模块缓存(Module Caching)

模块缓存是 CommonJS 模块系统的基石之一,由 Module._cache 静态对象实现。

缓存原理:

  1. myRequirelocalRequire 被调用时,首先会尝试将请求的模块路径解析为绝对路径。
  2. 然后,它会检查 Module._cache 中是否已经存在这个绝对路径对应的模块。
  3. 如果存在,直接返回缓存中的 module.exports 对象,而不会再次读取文件或执行模块代码。
  4. 如果不存在,则创建一个新的 Module 实例,将其加入缓存,然后加载并执行模块代码,最后返回其 exports

为什么重要:

  • 避免重复工作: 模块代码只执行一次,节省 CPU 和 I/O 资源。
  • 状态共享: 如果一个模块维护了内部状态(例如,一个单例数据库连接),所有 require 它的地方都会得到同一个实例,从而共享这个状态。
  • 循环依赖处理: 当一个模块 A 引用 B,B 又引用 A 时,缓存机制允许 A 在 B require A 时,立即返回 A 已经缓存的 exports 对象(即使 A 尚未完全执行完毕)。这避免了无限循环,尽管返回的 exports 对象可能还不完整。

示例:

// moduleA.js
// 假设这是通过 myRequire 加载的
console.log('moduleA 开始执行');
let counter = 0;
module.exports.increment = () => ++counter;
module.exports.getCounter = () => counter;
console.log('moduleA 执行完毕');

// main.js 中模拟调用
const myRequire = require('./my_loader').myRequire; // 假设 my_loader.js 导出了 myRequire
const a1 = myRequire('./modules/moduleA.js'); // 假设存在 './modules/moduleA.js'
const a2 = myRequire('./modules/moduleA.js'); // 从缓存中获取

console.log('a1 === a2:', a1 === a2); // true
console.log('Counter after a1.increment():', a1.increment()); // 1
console.log('Counter from a2.getCounter():', a2.getCounter()); // 1 (共享状态)

// 预期输出:
// moduleA 开始执行
// moduleA 执行完毕
// a1 === a2: true
// Counter after a1.increment(): 1
// Counter from a2.getCounter(): 1

从输出可以看出,moduleA 开始执行moduleA 执行完毕 只打印了一次,证明了模块只执行一次。a1a2 引用的是同一个对象,并且共享了 counter 变量的状态。

3.3. 导出机制:exportsmodule.exports

这是 CommonJS 中一个经常让初学者感到困惑的地方。理解它们的区别至关重要。

核心规则: require 函数最终返回的是 module.exports 对象的值。

初始状态:
当模块代码被包装器函数执行时,exportsmodule.exports 都指向同一个空对象 {}

(function(exports, require, module, __filename, __dirname) {
  // 在模块内部,初始时:
  // exports === module.exports // true
  // exports 和 module.exports 都指向同一个空对象 {}
  // ... 模块代码 ...
});

两种导出方式:

  1. 通过 exports 对象的属性添加:
    当你通过 exports.propertyName = value 的方式导出时,实际上是在修改 exports(以及 module.exports)所指向的同一个对象的属性。这种方式适用于导出多个具名成员。

    // myModule.js
    exports.foo = 'hello';
    exports.bar = function() { return 'world'; };
    // require('./myModule') 将返回 { foo: 'hello', bar: [Function] }
  2. 直接赋值给 module.exports
    当你直接将一个值赋值给 module.exports 时,你实际上是改变了 module.exports 的引用,使其不再指向原来的空对象,而是指向你赋给它的新值。此时,exports 仍然指向原来的空对象,但 require 最终返回的是新的 module.exports。这种方式适用于导出单个值(如函数、类、数组或任意对象)。

    // myModule.js (导出单个函数)
    module.exports = function() {
      console.log('This is a function exported directly.');
    };
    // require('./myModule') 将直接返回这个函数
    
    // anotherModule.js (导出单个对象)
    const myData = { name: 'Alice', age: 30 };
    module.exports = myData;
    // require('./anotherModule') 将直接返回 myData 对象

注意事项(陷阱):

  • 直接赋值给 exports 将会失效!
    如果你尝试 exports = { foo: 'bar' },这仅仅是将 exports 变量重新指向了一个新的对象。module.exports 仍然指向最初的那个空对象,因此 require 将不会返回你期望的新对象。

    // badModule.js
    exports = {
      name: 'Bob' // 这不会生效!
    };
    exports.age = 25; // 这也不会生效,因为它修改的是新的 exports 对象,而不是 module.exports 指向的原始对象
    module.exports.gender = 'male'; // 这会生效,因为直接修改 module.exports 指向的对象
    
    // main.js
    const m = myRequire('./badModule.js'); // 假设存在 './badModule.js'
    console.log(m); // 预期输出: { gender: 'male' }
    // name 和 age 不会被导出!

    解释: 模块包装器函数接收的是 exports 对象的引用。当 exports = { name: 'Bob' } 时,你只是改变了函数作用域内局部变量 exports 的指向,使其不再指向 module.exports 所指向的对象。而 module.exports 仍然指向最初传入的那个对象。因此,外部 require 看到的 module.exports 没有改变。

    正确做法: 始终通过 module.exports 进行整体赋值。

    // goodModule.js
    module.exports = {
      name: 'Bob',
      age: 25
    };
    // require('./goodModule') 将返回 { name: 'Bob', age: 25 }

总结表格:exportsmodule.exports 的行为

操作 module.exports 的值 exports 的值 require() 返回的值 备注
初始状态 {} {} N/A (尚未返回) exportsmodule.exports 指向同一个对象
exports.foo = 'bar' { foo: 'bar' } { foo: 'bar' } { foo: 'bar' } 修改了共享对象的属性
module.exports = { baz: 'qux' } { baz: 'qux' } {} (仍指向原始空对象) { baz: 'qux' } module.exports 引用被改变,exports 不受影响
exports = { foo: 'bar' } {} (仍指向原始空对象) { foo: 'bar' } (新的对象) {} (仍是原始空对象) 错误用法! 仅仅是局部变量赋值,不会影响 module.exports
module.exports.foo = 'bar' { foo: 'bar' } { foo: 'bar' } { foo: 'bar' } 直接修改 module.exports 指向的对象的属性

4. 实践演示:使用我们的加载器

为了演示我们实现的 CommonJS 模块加载器,我们创建几个示例文件。

文件结构:

.
├── my_loader.js         // 我们的 Module 类和 myRequire 函数
├── main.js              // 入口文件
├── modules
│   ├── a.js
│   ├── b.js
│   ├── c.js
│   └── data.json
└── node_modules
    └── my-utility
        └── index.js

my_loader.js (就是上面我们实现的所有 Module 类和 myRequire 函数的代码)

// (此处省略 my_loader.js 的完整代码,因为它已在前面给出)
// ... (上面定义的 Module 类和 myRequire 函数代码) ...

modules/a.js

// modules/a.js
console.log('Module A loading...');
const b = require('./b'); // 相对路径 require
console.log('Module A requires module B');

module.exports = {
  name: 'Module A',
  fromB: b.name,
  getBValue: b.getValue,
  counter: b.getCounter,
  incrementB: b.increment,
  jsonData: b.jsonData // 传递 B 模块中的 JSON 数据
};
console.log('Module A loaded.');

modules/b.js

// modules/b.js
console.log('Module B loading...');
const c = require('./c'); // 相对路径 require
const data = require('./data.json'); // JSON 文件 require
console.log('Module B requires module C and data.json');

let value = 100;
let callCount = 0;

exports.name = 'Module B';
exports.getValue = () => value;
exports.increment = () => {
  callCount++;
  return ++value;
};
exports.getCounter = () => callCount;
exports.cName = c.name;
exports.jsonData = data;

console.log('Module B loaded.');

modules/c.js

// modules/c.js
console.log('Module C loading...');

exports.name = 'Module C';
exports.version = '1.0.0';

console.log('Module C loaded.');

modules/data.json

{
  "key": "value from json",
  "number": 123
}

node_modules/my-utility/index.js

// node_modules/my-utility/index.js
console.log('My-utility loading...');

module.exports = {
  utilName: 'My Utility',
  doSomething: () => 'Doing something useful!'
};

console.log('My-utility loaded.');

main.js (入口文件)

// main.js
// 引入我们自己的模块加载器
const { Module, myRequire } = require('./my_loader'); 
const vm = require('vm'); // 引入 vm 模块用于模拟 exports vs module.exports 演示

console.log('Main application started.');

// 加载 modules/a.js
const moduleA = myRequire('./modules/a');
console.log('--- main.js loaded module A ---');
console.log('moduleA.name:', moduleA.name);
console.log('moduleA.fromB:', moduleA.fromB);
console.log('moduleA.getBValue():', moduleA.getBValue());
console.log('moduleA.counter():', moduleA.counter()); // callCount should be 0 initially
console.log('moduleA.incrementB():', moduleA.incrementB()); // value in B increments
console.log('moduleA.getBValue() after increment:', moduleA.getBValue());
console.log('moduleA.counter() after increment:', moduleA.counter()); // callCount should be 1
console.log('moduleA.jsonData:', moduleA.jsonData); // Access nested JSON data from Module A, which got it from Module B

console.log('n--- Demonstrating caching ---');
const moduleA_cached = myRequire('./modules/a');
console.log('moduleA === moduleA_cached:', moduleA === moduleA_cached); // Should be true

console.log('n--- Loading an npm-style module ---');
const myUtility = myRequire('my-utility'); // 模拟加载 npm 模块
console.log('myUtility.utilName:', myUtility.utilName);
console.log('myUtility.doSomething():', myUtility.doSomething());

console.log('n--- Demonstrating exports vs module.exports ---');
// 为了简化,我们直接在这里模拟一个模块的 _compile 过程来观察
const fakeModule = new Module('fake_module_id', null);
let fakeExports = fakeModule.exports; // 初始 {}

const testCode = `
  exports.a = 1;
  module.exports.b = 2; // 此时 module.exports 和 exports 依然指向同一个对象
  module.exports = { c: 3 }; // module.exports 重新赋值,exports 仍指向旧对象
  exports.d = 4; // 此时 exports 仍指向旧对象,这里修改的是旧对象,不会被 require 返回
`;

const wrappedTestCode = `(function(exports, require, module, __filename, __dirname) {n${testCode}n});`;
const compiledTestCode = vm.runInThisContext(wrappedTestCode, { filename: 'testExports.js' });
compiledTestCode.call(fakeExports, fakeExports, () => {}, fakeModule, 'testExports.js', '.');

console.log('Result from exports vs module.exports test:', fakeModule.exports); // 应该输出 { c: 3 }

console.log('nMain application finished.');

如何运行:
在项目根目录(与 my_loader.jsmain.js 同级)下,执行:
node main.js

预期输出:

Main application started.
Module B loading...
Module C loading...
Module C loaded.
Module B requires module C and data.json
Module B loaded.
Module A loading...
Module A requires module B
Module A loaded.
--- main.js loaded module A ---
moduleA.name: Module A
moduleA.fromB: Module B
moduleA.getBValue(): 100
moduleA.counter(): 0
moduleA.incrementB(): 101
moduleA.getBValue() after increment: 101
moduleA.counter() after increment: 1
moduleA.jsonData: { key: 'value from json', number: 123 }

--- Demonstrating caching ---
moduleA === moduleA_cached: true

--- Loading an npm-style module ---
My-utility loading...
My-utility loaded.
myUtility.utilName: My Utility
myUtility.doSomething(): Doing something useful!

--- Demonstrating exports vs module.exports ---
Result from exports vs module.exports test: { c: 3 }

Main application finished.

这个输出清晰地展示了:

  1. 加载顺序: main.js 引用 a.jsa.js 引用 b.jsb.js 引用 c.jsdata.json。因此,模块加载的顺序是 C -> data.json -> B -> A
  2. 同步性: 模块的 console.log 语句是按顺序立即执行的,证明了同步加载。
  3. 缓存: 第二次 require('./modules/a') 时,Module A loading... 不会再次打印,且 moduleA === moduleA_cachedtrue,证明了缓存生效。
  4. 状态共享: moduleA.incrementB() 改变了 modules/b.js 内部的 value 变量,并且 moduleA.getBValue()moduleA.counter() 都能正确反映这些变化。
  5. exports vs module.exports 最后的手动模拟演示了当 module.exports 被重新赋值后,require 最终只会返回 module.exports 的新值。

5. 模块化机制的精髓

我们实现的 CommonJS 模块加载器,尽管是简化的,但它捕捉了 Node.js 模块系统的核心精髓。一个完整的 CommonJS 实现还需要处理更复杂的路径解析、原生模块加载、更健壮的错误处理以及性能优化。然而,通过这次深入的探讨和实践,我们已经清晰地理解了 require 函数背后的同步加载、模块缓存和导出机制。这些基础知识为我们驾驭更复杂的 JavaScript 生态系统,以及理解 ES Modules 等现代模块化规范奠定了坚实的基础。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注