招聘 负责网站开发免费海外云服务器

张小明 2026/1/9 15:01:50
招聘 负责网站开发,免费海外云服务器,济南网站的建设,大数据培训机构可信吗各位同仁#xff0c;下午好#xff01;今天#xff0c;我们将深入探讨 Node.js 中 CommonJS 模块系统的核心机制之一#xff1a;模块缓存。这是一个看似简单却蕴含深厚设计哲学的机制#xff0c;它直接决定了我们在 Node.js 应用中管理状态、优化性能以及理解模块行为的关…各位同仁下午好今天我们将深入探讨 Node.js 中 CommonJS 模块系统的核心机制之一模块缓存。这是一个看似简单却蕴含深厚设计哲学的机制它直接决定了我们在 Node.js 应用中管理状态、优化性能以及理解模块行为的关键。我们的核心问题是为什么对同一个模块进行多次require调用时我们总是得到同一个对象要解答这个问题我们需要一层层拨开require函数的神秘面纱从模块的加载、编译到最终的导出和缓存全面剖析其内部工作原理。Node.js 模块系统的基石CommonJS 规范在 Node.js 的早期和大部分现有项目中CommonJS 规范是模块化的基石。它定义了模块如何被定义、导出和导入。其核心思想是每个文件都被视为一个独立的模块拥有自己的作用域。当我们谈论 CommonJS 模块时最常涉及的两个全局对象就是module和exports。module对象代表当前模块的元数据其中最重要的属性是module.exports它定义了当前模块对外暴露的内容。exports对象它是module.exports的一个引用在模块代码执行的初始阶段。通常我们通过向exports对象添加属性来导出多个功能或者直接替换module.exports来导出一个单一的值例如一个类或一个函数。// myModule.js let counter 0; function increment() { counter; return counter; } function decrement() { counter--; return counter; } console.log(myModule.js is being evaluated!); // 这行代码在模块首次加载时执行 // 导出多个功能 exports.increment increment; exports.decrement decrement; exports.currentCounter () counter; // 导出当前计数器的getter在上面的例子中myModule.js维护了一个内部状态counter。它通过exports对象对外暴露了几个函数和方法。值得注意的是console.log语句会告诉我们模块何时被“评估”或“执行”。CommonJS 的模块加载是同步的。这意味着当require(some-module)被调用时Node.js 会暂停当前代码的执行直到some-module被完全加载、编译和执行完毕并返回其导出的内容。这种同步性在服务器端环境中通常不是问题但在浏览器环境中可能会导致阻塞。require函数的幕后之旅第一次加载当 Node.js 首次遇到一个require()调用时它会经历一个复杂而精妙的过程。这个过程大致可以分为三个阶段解析 (Resolution)、加载与编译 (Loading Compilation)和缓存 (Caching)。1. 模块路径解析 (Module Resolution)require函数首先需要确定要加载的模块文件的绝对路径。这个过程被称为模块解析。Node.js 有一套明确的解析规则它会根据require调用中传入的路径字符串来查找对应的文件。核心模块如果传入的字符串是 Node.js 内置的核心模块如fs,http,path等Node.js 会直接加载其内置的二进制实现。文件模块如果路径以./、../或/开头Node.js 会将其视为文件路径或目录路径并尝试在指定位置查找。它会尝试添加.js,.json,.node等常见扩展名。如果路径指向一个目录Node.js 会查找该目录下的package.json文件。如果package.json中定义了main字段则加载main字段指定的文件。如果package.json不存在或没有main字段则尝试加载index.js、index.json或index.node。node_modules模块如果路径不包含任何路径前缀例如require(lodash)Node.js 会将它视为一个第三方模块。它会从当前文件所在的目录开始逐级向上查找node_modules目录直到文件系统的根目录。在每个node_modules目录中它会查找与模块名同名的子目录然后按照文件模块的规则package.json的main字段或index.js等加载模块。这个解析过程是同步的并且非常高效。Node.js 内部维护了一个文件系统缓存以加速重复的路径查找。我们可以使用require.resolve()函数来查看模块的解析结果它不会实际加载模块只会返回模块的绝对路径。// main.js console.log(Path module resolved to:, require.resolve(path)); console.log(Lodash module resolved to:, require.resolve(lodash)); // 假设已安装lodash // console.log(Non-existent module resolved to:, require.resolve(non-existent-module)); // 会抛出错误 // 假设我们有一个 ./utils/helper.js 文件 // utils/helper.js // console.log(Helper loaded); // main.js console.log(Local helper module resolved to:, require.resolve(./utils/helper.js)); /* 输出示例: Path module resolved to: /Users/youruser/nvm/versions/node/v18.17.1/lib/node_modules/path/path.js Lodash module resolved to: /Users/youruser/my-project/node_modules/lodash/lodash.js Local helper module resolved to: /Users/youruser/my-project/utils/helper.js */require.resolve帮助我们理解 Node.js 如何定位模块文件它是require函数内部解析阶段的关键步骤。一旦模块的绝对路径被确定这个路径将作为模块的唯一标识符Module ID在后续的缓存机制中发挥作用。2. 模块加载与编译 (Module Loading Compilation)一旦模块的绝对路径被确定Node.js 就会执行以下步骤读取文件内容Node.js 会同步地从磁盘读取模块文件的内容。模块包装器 (Module Wrapper)这是 CommonJS 模块系统的核心魔法之一。Node.js 不会直接执行模块文件的原始代码。相反它会将模块的代码用一个函数包装起来。这个包装器函数提供了模块的私有作用域并注入了exports、require、module、__filename和__dirname这五个重要的变量使得它们在模块内部可用。这个包装器函数看起来大致如下(function(exports, require, module, __filename, __dirname) { // 你的模块代码在这里 // 例如 // let counter 0; // exports.increment () counter; });通过这个包装器每个 CommonJS 模块都在一个独立的函数作用域中运行避免了全局变量污染并确保了模块的封装性。exports、require、module等变量都是局部于这个函数作用域的它们是 Node.js 运行时提供的特定实例。让我们通过一个简单实验来“揭示”这个包装器// wrapperTest.js console.log(Is exports module.exports?, exports module.exports); console.log(Type of require:, typeof require); console.log(Type of module:, typeof module); console.log(Current filename:, __filename); console.log(Current dirname:, __dirname); // 尝试访问全局变量它不会被污染 // console.log(globalVarDefinedInOtherModule); // Uncaught ReferenceError当我们在 Node.js 中运行node wrapperTest.js时你会看到exports确实是module.exports的一个引用require和module是可用的对象并且__filename和__dirname指向当前模块的文件路径和目录。这些都是包装器函数提供的上下文。模块执行Node.js 会调用这个包装器函数并将相应的exports、require、module、__filename和__dirname对象作为参数传入。此时模块内部的代码开始执行。模块中定义的变量和函数都将局限于这个函数作用域内除非它们被显式地挂载到exports或module.exports上。在模块执行期间如果模块内部又调用了require来加载其他模块那么这个过程会递归地重复。3. 模块导出与缓存 (Module Export and Caching)当模块代码执行完毕后module.exports对象中包含的内容就是该模块最终对外暴露的接口。Node.js 会将这个module.exports对象存储在一个内部的缓存中。这个缓存就是require.cache。require.cache是一个 JavaScript 对象它的键是模块的绝对路径即前面解析阶段确定的模块 ID值是对应的module对象。每个module对象都有一个exports属性即模块最终导出的内容。module对象还有一个loaded属性一个布尔值表示该模块是否已经完成加载和执行。让我们通过一个具体的例子来观察这个过程// moduleA.js // 这是一个模拟耗时操作确保我们能观察到模块只执行一次 for (let i 0; i 1e7; i) {} // 模拟一些计算 console.log(ModuleA is being evaluated!); let count 0; exports.increment () count; exports.currentCount () count;// main.js console.log(--- First require call ---); const modA1 require(./moduleA.js); console.log(modA1.currentCount():, modA1.currentCount()); // 0 console.log(--- Second require call ---); const modA2 require(./moduleA.js); console.log(modA2.currentCount():, modA2.currentCount()); // 0 modA1.increment(); console.log(After incrementing via modA1:); console.log(modA1.currentCount():, modA1.currentCount()); // 1 console.log(modA2.currentCount():, modA2.currentCount()); // 1 console.log(n--- Inspecting require.cache ---); // 获取 moduleA.js 的绝对路径这是它在缓存中的键 const moduleAPath require.resolve(./moduleA.js); console.log(moduleAPath:, moduleAPath); console.log(Is moduleA in cache?, !!require.cache[moduleAPath]); // 我们可以直接访问缓存中的 module 对象 console.log(Cached moduleA exports:, require.cache[moduleAPath].exports); console.log(Are modA1 and cached exports the same object?, modA1 require.cache[moduleAPath].exports); /* 输出示例: --- First require call --- ModuleA is being evaluated! modA1.currentCount(): 0 --- Second require call --- modA2.currentCount(): 0 After incrementing via modA1: modA1.currentCount(): 1 modA2.currentCount(): 1 --- Inspecting require.cache --- moduleAPath: /Users/youruser/my-project/moduleA.js Is moduleA in cache? true Cached moduleA exports: { increment: [Function: increment], currentCount: [Function: currentCount] } Are modA1 and cached exports the same object? true */从上面的输出中我们可以清晰地看到ModuleA is being evaluated!只输出了一次这证明moduleA.js的代码只被执行了一次。modA1和modA2尽管是两次require调用得到的结果但它们实际上是同一个对象通过modA1.increment()修改的状态在modA2中也反映了出来。modA1与require.cache[moduleAPath].exports严格相等进一步证实了require返回的是缓存中的module.exports对象。核心机制揭秘二次require为何得到同一个对象现在我们终于可以直面核心问题了。当 Node.js 遇到一个require()调用时其内部逻辑会遵循一个简单的优先级规则缓存查找优先原则require函数在执行任何文件 I/O 或代码编译之前会首先检查require.cache对象。它会根据要加载模块的解析后的绝对路径Module ID去查找缓存中是否存在对应的模块。模块 ID 与缓存键正如我们前面提到的模块的绝对路径就是其在require.cache中的唯一键。例如如果require(./myModule.js)经过解析得到/Users/username/project/myModule.js那么这个字符串就是require.cache中的键。返回缓存结果如果缓存中找到了该模块require函数会直接返回require.cache[moduleID].exports对象。它不会重新读取文件、重新编译代码也不会再次执行模块中的任何逻辑。这就是为什么myModule.js is being evaluated!只会打印一次的原因。如果缓存中没有找到该模块那么 Node.js 才会执行前面提到的“解析 - 加载与编译 - 首次缓存”的完整流程并将新加载的模块及其导出的内容存入缓存然后返回该内容。这个缓存查找优先的原则是 CommonJS 模块缓存机制的核心。它确保了性能优化避免了重复的文件 I/O 操作和 JavaScript 代码的重复解析与执行大大提高了应用程序的启动速度和运行时效率。状态共享与单例模式确保了同一个模块在整个应用程序生命周期内只会被加载一次。如果模块内部维护了状态例如计数器、数据库连接池、配置对象等那么所有require该模块的地方都将共享同一个状态实例。这使得 CommonJS 模块非常适合实现单例模式。让我们用一个表格来对比首次require和后续require的流程差异| 步骤 | 首次require(moduleX)| 后续require(moduleX)CommonJS CommonJS is Node.js for Node.jscommon common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common common CommonJS CommonJS 的缓存机制为什么二次require得到的对象是同一个各位 Node.js 开发者和技术爱好者大家好今天我们将深入剖析 NodeJS CommonJS 模块系统的核心特性之一require函数的缓存机制。这是一个至关重要的细节它直接影响着我们应用程序的性能、状态管理以及模块的行为模式。我们将解答一个核心问题为什么对同一个模块路径进行多次require调用时我们总是得到同一个对象为了彻底理解这一点我们将从 CommonJS 模块规范的基础出发逐步深入到 Node.js 内部的模块加载流程剖析require.cache的作用并通过丰富的代码示例来验证和巩固我们的理解。1. CommonJS 模块规范回顾与require的作用在 Node.js 环境中每个文件都被视为一个独立的模块。CommonJS 规范定义了模块的导入和导出方式使得开发者可以组织代码避免全局作用域污染并实现代码的复用。核心概念模块作用域每个 CommonJS 模块都有自己独立的作用域。模块内部定义的变量、函数等除非显式导出否则不会暴露给其他模块。module.exports与exportsmodule.exports是模块对外暴露接口的真正载体。它是一个对象默认情况下是一个空对象{}。exports是module.exports的一个引用在模块代码执行的初始阶段。通常我们可以通过向exports对象添加属性来导出多个功能。当需要导出一个单一的值如一个类、一个函数或一个原始值时我们通常会直接赋值给module.exports这会切断exports和module.exports之间的引用关系。require函数这是 Node.js 中用于导入模块的全局函数。它接收一个模块标识符通常是文件路径或模块名作为参数并同步地返回被导入模块导出的内容。让我们看一个简单的 CommonJS 模块示例// lib/my-module.js console.log([my-module.js] 模块代码开始执行...); let privateData 10; // 模块内部私有状态 function add(a, b) { return a b; } function subtract(a, b) { return a - b; } // 通过 exports 对象导出多个功能 exports.add add; exports.subtract subtract; exports.version 1.0.0; // 也可以直接修改 module.exports这会覆盖 exports // module.exports { // myFunction: () console.log(This is a single function export!) // }; console.log([my-module.js] 模块代码执行完毕。);这个my-module.js文件就是一个 CommonJS 模块。当它被require时其中的console.log语句会执行privateData会被初始化add、subtract函数会被定义并通过exports对象对外暴露。2.require函数的深层解析模块加载的完整生命周期当我们在主文件中首次调用require(./lib/my-module.js)时Node.js 会执行一个详细的模块加载过程。这个过程可以概括为以下几个主要阶段2.1. 模块路径解析 (Module Resolution)这是require函数的第一步也是至关重要的一步。Node.js 必须根据传入的模块标识符确定要加载的模块文件的绝对路径。Node.js 遵循一套严格的解析算法核心模块优先如果标识符是 Node.js 内置的核心模块如fs,http,path,util等Node.js 会直接加载其内置的 C 或 JavaScript 实现跳过文件系统查找。文件路径或目录路径如果标识符以./、../或/开头Node.js 会尝试将其解析为文件或目录路径。文件它会尝试直接加载该文件。如果文件不存在它会尝试添加常见的扩展名.js、.json、.node依次尝试。目录如果标识符指向一个目录Node.js 会查找该目录下的package.json文件。如果package.json存在且包含main字段则加载main字段指定的文件再次尝试添加扩展名。如果package.json不存在或没有main字段则尝试加载index.js、index.json或index.node。node_modules查找如果标识符既不是核心模块也不是相对/绝对路径例如require(lodash)Node.js 会将其视为一个第三方模块或包。它会从当前模块文件所在的目录开始逐级向上查找名为node_modules的目录。在每个node_modules目录中它会查找与模块标识符同名的子目录。一旦找到它会按照上述“目录路径”的规则package.json的main字段或index.js等加载模块。示例使用require.resolve()require.resolve()是一个非常有用的工具它允许我们模拟require函数的解析过程返回模块的绝对路径但不会实际加载和执行模块代码。// main.js const path require(path); const fs require(fs); console.log(--- 模块路径解析示例 ---); // 1. 核心模块 console.log(Path module resolved to:, require.resolve(path)); // 输出 Node.js 内置 path 模块的路径 // 2. 第三方模块 (假设你已安装 lodash) try { console.log(Lodash module resolved to:, require.resolve(lodash)); } catch (e) { console.log(Lodash not found, please install with npm install lodash); } // 3. 本地文件模块 // 创建一个文件 ./modules/util.js // modules/util.js // module.exports { greet: () Hello from util! }; fs.mkdirSync(./modules, { recursive: true }); fs.writeFileSync(./modules/util.js, module.exports { greet: () Hello from util! };); console.log(Local util module resolved to:, require.resolve(./modules/util.js)); // 4. 本地目录模块 (假设有 ./modules/calculator/index.js) // modules/calculator/index.js // module.exports { add: (a, b) a b }; fs.mkdirSync(./modules/calculator, { recursive: true }); fs.writeFileSync(./modules/calculator/index.js, module.exports { add: (a, b) a b };); console.log(Local calculator directory module resolved to:, require.resolve(./modules/calculator)); // 清理创建的测试文件 fs.rmSync(./modules, { recursive: true, force: true }); /* 可能的输出: --- 模块路径解析示例 --- Path module resolved to: /usr/local/lib/node_modules/path/path.js Lodash module resolved to: /Users/youruser/your-project/node_modules/lodash/lodash.js Local util module resolved to: /Users/youruser/your-project/modules/util.js Local calculator directory module resolved to: /Users/youruser/your-project/modules/calculator/index.js */一旦模块的绝对路径被确定这个路径就成为了该模块在 Node.js 运行时中的唯一标识符Module ID。这个 Module ID 将是后续缓存机制中的关键。2.2. 模块加载与编译 (Module Loading Compilation)在解析阶段确定了模块的绝对路径后Node.js 会执行以下步骤来加载和编译模块读取文件内容Node.js 会同步地从磁盘读取模块文件的完整内容以字符串形式。模块包装器 (Module Wrapper)这是 CommonJS 模块实现其独立作用域的关键。Node.js 不会直接执行读取到的原始 JavaScript 代码。相反它会将模块的代码用一个特殊的函数包装起来。这个包装器函数提供了一个私有作用域并向模块代码注入了五个重要的局部变量exports,require,module,__filename, 和__dirname。这个包装器函数的结构大致如下(function(exports, require, module, __filename, __dirname) { // 模块的原始代码在这里被插入 // 例如 // console.log([my-module.js] 模块代码开始执行...); // let privateData 10; // exports.add (a, b) a b; });通过这种包装模块内部的所有变量和函数都局限于这个函数作用域不会污染全局环境同时又能方便地访问到require、exports等 Node.js 提供的模块化工具。模块执行Node.js 接下来会调用这个包装器函数并将当前模块对应的exports对象、require函数、module对象以及__filename和__dirname字符串作为参数传递进去。此时模块的实际代码开始执行。在模块执行期间模块内部可以定义变量和函数。模块可以通过exports或module.exports来设置对外暴露的接口。模块内部也可以调用require来导入其他模块这会触发一个递归的加载过程。2.3. 模块导出与缓存 (Module Export and Caching)当模块的包装器函数执行完毕即模块代码完全运行结束后module.exports对象中包含的内容就是该模块最终对外暴露的接口。Node.js 会将这个module.exports对象存储在一个内部的缓存中。这个缓存就是require.cache。require.cache是一个普通的 JavaScript 对象它存储了所有已加载模块的引用。缓存键 (Key)缓存的键是模块的绝对路径即前面解析阶段确定的 Module ID。缓存值 (Value)缓存的值是对应的Module对象本身。这个Module对象包含了模块的元数据其中最重要的就是exports属性它持有该模块最终导出的内容。Module对象还有一个loaded属性布尔值表示模块是否已完成加载和执行。// lib/cached-module.js console.log([cached-module.js] 模块被首次评估); // 观察此行只输出一次 let counter 0; exports.increment () counter; exports.getCurrent () counter;// main.js console.log(--- 首次 require 调用 ---); const mod1 require(./lib/cached-module.js); console.log(mod1.getCurrent():, mod1.getCurrent()); // 0 console.log(n--- 检查 require.cache ---); // 获取模块的绝对路径作为缓存键 const modulePath require.resolve(./lib/cached-module.js); console.log(cached-module.js 的绝对路径:, modulePath); // 检查缓存中是否存在 const cachedModuleEntry require.cache[modulePath]; console.log(缓存中是否存在该模块:, !!cachedModuleEntry); // true if (cachedModuleEntry) { console.log(缓存中的 exports 对象:, cachedModuleEntry.exports); console.log(mod1 cachedModuleEntry.exports?, mod1 cachedModuleEntry.exports); // true console.log(缓存中的 Module 对象是否已加载:, cachedModuleEntry.loaded); // true } console.log(n--- 第二次 require 调用 ---); const mod2 require(./lib/cached-module.js); // 注意不会再次打印 模块被首次评估 console.log(mod2.getCurrent():, mod2.getCurrent()); // 0 mod1.increment(); // 通过 mod1 修改模块内部状态 console.log(n--- 通过 mod1 调用 increment 后 ---); console.log(mod1.getCurrent():, mod1.getCurrent()); // 1 console.log(mod2.getCurrent():, mod2.getCurrent()); // 1 (mod2 也看到了状态变化) console.log(mod1 mod2?, mod1 mod2); // true // 清理测试文件 const fs require(fs); fs.rmSync(./lib, { recursive: true, force: true }); /* 输出示例: --- 首次 require 调用 --- [cached-module.js] 模块被首次评估 mod1.getCurrent(): 0 --- 检查 require.cache --- cached-module.js 的绝对路径: /Users/youruser/your-project/lib/cached-module.js 缓存中是否存在该模块: true 缓存中的 exports 对象: { increment: [Function: increment], getCurrent: [Function: getCurrent] } mod1 cachedModuleEntry.exports? true 缓存中的 Module 对象是否已加载: true --- 第二次 require 调用 --- mod2.getCurrent(): 0 --- 通过 mod1 调用 increment 后 --- mod1.getCurrent(): 1 mod2.getCurrent(): 1 mod1 mod2? true */从这个详细的示例中我们清晰地观察到[cached-module.js] 模块被首次评估这条日志只在第一次require调用时出现证实了模块代码只执行一次。mod1和mod2确实是同一个对象实例mod1 mod2为true。通过mod1修改模块内部状态后这种变化也通过mod2反映出来这进一步证明了它们共享同一个底层模块实例。mod1严格等于require.cache中存储的exports对象这揭示了require返回值的来源。3.require.cache缓存机制的核心require.cache是一个全局对象它扮演着 CommonJS 模块系统缓存的心脏角色。它的结构是一个简单的键值对映射键 (Key)模块的绝对路径。值 (Value)完整的Module对象。每个Module对象至少包含以下关键属性id: 模块的绝对路径与缓存键相同。exports: 模块最终导出的内容。这就是require函数的返回值。parent: 引用这个模块的父模块。filename: 模块文件的绝对路径。loaded: 一个布尔值表示模块是否已完成加载和执行。children: 一个数组包含这个模块所require的所有子模块。require函数的完整逻辑流程简版解析模块标识符得到模块的绝对路径moduleID。检查require.cache[moduleID]如果存在直接返回require.cache[moduleID].exports。如果不存在a. 创建一个新的Module实例并将其存储到require.cache[moduleID]。b. 读取模块文件内容。c. 用包装器函数包裹模块代码。d. 执行包裹后的模块代码。e. 将module.exports的最终值赋给require.cache[moduleID].exports。f. 将require.cache[moduleID].loaded设置为true。g. 返回require.cache[moduleID].exports。通过这个流程Node.js 确保了任何模块在整个应用生命周期中只要其模块 ID 相同就只会被加载和执行一次。后续对相同模块的require调用都将从require.cache中获取其导出的exports对象。3.1. 手动操作require.cache一个双刃剑理论上我们可以直接操作require.cache对象。例如我们可以删除缓存中的某个模块条目从而强制 Node.js 在下次require时重新加载该模块。// lib/reset-module.js console.log([reset-module.js] 模块被评估); let value Math.random(); // 每次评估生成一个新随机数 exports.getValue () value;// main.js const path require(path); const fs require(fs); fs.mkdirSync(./lib, { recursive: true }); fs.writeFileSync(./lib/reset-module.js, console.log([reset-module.js] 模块被评估); let value Math.random(); exports.getValue () value; ); console.log(--- 第一次加载 ---); const modA require(./lib/reset-module.js); console.log(modA.getValue():, modA.getValue()); // 会得到一个随机数 console.log(n--- 第二次加载 (从缓存) ---); const modB require(./lib/reset-module.js); console.log(modB.getValue():, modB.getValue()); // 得到和 modA 相同的值 (因为是从缓存读取) console.log(modA modB?, modA modB); // true console.log(n--- 清除缓存并重新加载 ---); const modulePathToClear require.resolve(./lib/reset-module.js); delete require.cache[modulePathToClear]; // 从缓存中删除该模块 console.log(缓存已清除。); console.log(--- 第三次加载 (强制重新加载) ---); const modC require(./lib/reset-module.js); // 此时会再次打印 [reset-module.js] 模块被评估 console.log(modC.getValue():, modC.getValue()); // 得到一个新的随机数 console.log(modA modC?, modA modC); // false // 清理测试文件 fs.rmSync(./lib, { recursive: true, force: true }); /* 输出示例: --- 第一次加载 --- [reset-module.js] 模块被评估 modA.getValue(): 0.7328994840822607 --- 第二次加载 (从缓存) --- modB.getValue(): 0.7328994840822607 modA modB? true --- 清除缓存并重新加载 --- 缓存已清除。 --- 第三次加载 (强制重新加载) --- [reset-module.js] 模块被评估 modC.getValue(): 0.1234567890123456 modA modC? false */警告手动操作require.cache是一种非常规的做法通常只在开发环境中的热重载、测试或非常特殊的场景下使用。在生产环境中随意清除缓存可能会导致意外的行为、性能问题和内存泄漏因为它可能破坏模块之间预期的状态共享和单例模式。通常Node.js 应用程序的生命周期中不建议清除缓存。4. 缓存机制的设计哲学为什么这样做Node.js 采用 CommonJS 缓存机制并非偶然它背后蕴含着深刻的设计考量和优势性能优化避免重复文件 I/O读取文件是磁盘操作相对耗时。缓存机制确保文件只被读取一次。避免重复代码解析与编译将 JavaScript 源代码解析成抽象语法树AST并编译成字节码也是一个计算密集型过程。缓存避免了这些重复工作。避免重复模块执行模块内部可能包含复杂的初始化逻辑、数据库连接、网络请求等。重复执行这些逻辑会浪费资源并降低性能。状态共享与单例模式这是缓存机制最重要的副作用也是其有意为之的特性。当一个模块被require时它的module.exports对象被缓存。所有后续对该模块的require调用都将获得对同一个exports对象的引用。这意味着如果一个模块维护了内部状态例如一个配置对象、一个计数器、一个数据库连接池、一个事件发射器等那么所有导入该模块的消费者都将共享这个状态。这非常适合实现应用程序中的单例服务或配置管理。示例日志服务// services/logger.js console.log([logger.js] 初始化日志服务...); const logBuffer []; exports.log (message) { const entry ${new Date().toISOString()} - ${message}; logBuffer.push(entry); console.log(entry); }; exports.getLogs () [...logBuffer]; // 返回副本防止外部修改// app.js const logger require(./services/logger.js); const anotherComponent require(./components/anotherComponent.js); // 假设也 require 了 logger logger.log(应用启动); anotherComponent.doSomething(); // 假设这个方法也会调用 logger.log logger.log(应用关闭); console.log(n所有日志); logger.getLogs().forEach(log console.log(log));在这个例子中无论logger被require多少次logBuffer都是同一个数组实例。所有模块都会向同一个logBuffer写入日志确保了日志的集中管理。内存效率避免创建重复的模块对象和作用域减少了内存占用。防止副作用重复发生模块的初始化逻辑例如注册事件监听器、启动后台任务等通常只需要执行一次。缓存机制保证了这些副作用不会在每次require时重复触发。5. CommonJS 模块的特殊考量尽管缓存机制带来了诸多好处但在某些情况下我们也需要理解其可能带来的影响。5.1. 循环依赖 (Circular Dependencies)当模块 A 依赖模块 B同时模块 B 也依赖模块 A 时就形成了循环依赖。CommonJS 模块系统以一种特殊的方式处理这种情况当 ArequireB 时B 开始加载。在 B 执行过程中如果 BrequireA此时 A 尚未完全加载完毕module.loaded为false。Node.js 会直接返回 A 当前已导出的exports对象一个未完成填充的对象。B 继续执行然后完成加载并将其exports缓存。A 继续执行并最终完成加载其exports也会被填充完毕。示例// circularA.js console.log([A] circularA.js 开始加载); exports.name Module A; // A 模块先导出 name exports.aFunction () { console.log([A] aFunction 被调用); const b require(./circularB.js); // 此时 B 正在加载中 console.log([A] 在 aFunction 中访问 B 的 name:, b.name); // 可能会得到 undefined // 如果 b.bFunction 依赖 A 的完整导出这里也可能出现问题 }; console.log([A] circularA.js 导出完成);// circularB.js console.log([B] circularB.js 开始加载); exports.name Module B; // B 模块先导出 name exports.bFunction () { console.log([B] bFunction 被调用); const a require(./circularA.js); // 此时 A 正在加载中 console.log([B] 在 bFunction 中访问 A 的 name:, a.name); a.aFunction(); // 可能会导致无限循环或错误如果 aFunction 又 require b }; const a require(./circularA.js); // 这里的 a 可能会是一个不完整的 exports 对象 console.log([B] 在模块级别访问 A 的 name:, a.name); console.log([B] circularB.js 导出完成);// main.js const fs require(fs); fs.writeFileSync(./circularA.js, console.log([A] circularA.js 开始加载); exports.name Module A; exports.aFunction () { console.log([A] aFunction 被调用); const b require(./circularB.js); console.log([A] 在 aFunction 中访问 B 的 name:, b.name); }; console.log([A] circularA.js 导出完成); ); fs.writeFileSync(./circularB.js, console.log([B] circularB.js 开始加载); exports.name Module B; exports.bFunction () { console.log([B] bFunction 被调用); const a require(./circularA.js); console.log([B] 在 bFunction 中访问 A 的 name:, a.name); }; const a require(./circularA.js); console.log([B] 在模块级别访问 A 的 name:, a.name); console.log([B] circularB.js 导出完成); ); console.log(--- 启动主程序 ---); const modA require(./circularA.js); modA.aFunction(); // 清理文件 fs.rmSync(./circularA.js); fs.rmSync(./circularB.js); /* 输出示例: --- 启动主程序 --- [A] circularA.js 开始加载 [A] circularA.js 导出完成 [B] circularB.js 开始加载 [B] 在模块级别访问 A 的 name: Module A // 这里 A 已经导出 name [B] circularB.js 导出完成 [A] aFunction 被调用 [A] 在 aFunction 中访问 B 的 name: Module B // B 此时也已完成导出 */在上面的例子中[B] 在模块级别访问 A 的 name: Module A证明了circularA在被circularBrequire时其exports.name已经可用。这是因为circularA在require(./circularB.js)之前已经设置了exports.name Module A;。关键点CommonJS 循环依赖的处理方式是当一个模块被另一个模块require时它会返回当前已填充的exports对象即使模块尚未完全执行完毕。这意味着如果一个模块在它依赖的模块完全加载之前尝试访问该依赖模块的某个属性它可能会得到undefined或一个不完整的对象。因此设计模块时应尽量避免复杂的循环依赖。5.2.exports和module.exports的细微差别之前我们提到exports默认是module.exports的一个引用。但这种引用关系是可以被破坏的。保持引用如果我们通过exports.propertyName value;的方式导出那么exports和module.exports仍然是同一个对象。// moduleC.js exports.a 1; module.exports.b 2; // 此时 module.exports 是 { a: 1, b: 2 }断开引用如果我们直接给module.exports赋值例如module.exports someValue;那么exports就不再指向最终的导出对象。require函数只会返回module.exports的最终值。// moduleD.js exports.oldValue This will be ignored; // exports 仍然指向原来的空对象 module.exports { newValue: This is the actual export }; // 此时 require(./moduleD.js) 会得到 { newValue: This is the actual export } // exports 对象中的 oldValue 将不会被导出最佳实践为了避免混淆和潜在错误通常建议在一个模块中只使用一种导出风格要么始终通过exports.property value来添加属性要么始终通过module.exports someValue来完全替换导出对象。不要混用。6. 与 ES Modules 的对比 (简要)虽然我们主要讨论 CommonJS但简要提及 ES Modules (ESM) 的不同之处有助于加深理解。ESM 是 JavaScript 官方的模块化标准在 Node.js 中通过.mjs文件或在package.json中设置type: module来支持。特性CommonJS Modules (CJS)ES Modules (ESM)加载方式同步加载(require)异步加载(import)绑定方式值拷贝 (Value Copy)导入的是导出时的一个副本。但对于对象导入的是对象的引用因此对导入对象的修改会影响原始对象。实时绑定 (Live Binding)导入的是对原始模块变量的引用。当原始模块中的值改变时导入方也能看到这种变化。执行时机运行时加载和执行静态分析在代码执行前完成解析和绑定顶层作用域模块有自己的函数作用域 ((function(...){...}))模块有自己的文件作用域没有额外的函数包装缓存机制基于require.cache缓存module.exports对象的引用。基于模块注册表缓存模块的实例。模块同样只执行一次保证单例。循环依赖返回一个不完整的exports对象严格处理在绑定阶段就能检测并提供未初始化状态的引用虽然 ESM 也有其自身的缓存机制确保模块只执行一次但其“实时绑定”的特性使得 ESM 在处理导出值的变化时行为与 CommonJS 有所不同。对于对象而言CommonJS 导入的也是引用所以对对象的修改会影响原始对象但对于原始值如数字、字符串ESM 可以做到实时更新而 CommonJS 导入的是一个副本。7. CommonJS 缓存机制的实践意义深入理解 CommonJS 的缓存机制对于我们日常的 Node.js 开发具有重要的实践意义设计单例服务缓存机制是实现单例模式的天然基础。例如数据库连接池、日志记录器、配置管理器等都应该设计为单例以确保整个应用程序共享同一个实例。避免意外状态共享如果一个模块不应该共享状态例如一个工厂函数每次都应该返回新的实例那么就不能直接导出带有可变状态的对象。你需要确保每次require时返回一个新的实例例如通过导出一个工厂函数。优化应用程序启动时间意识到模块只加载一次可以帮助我们优化模块的初始化逻辑避免不必要的开销。理解错误行为当你在应用程序中遇到奇怪的状态不一致问题时检查是否是由于误解了模块缓存和状态共享导致的。总结CommonJS 模块的缓存机制是 Node.js 高效、可靠运行的基石。通过将模块的exports对象缓存到require.cache中Node.js 确保了对同一模块的多次require调用总是返回同一个对象实例。这一设计不仅极大地提升了性能避免了重复的文件 I/O 和代码执行更重要的是它为 Node.js 应用程序提供了强大的状态共享能力使得单例模式的实现变得自然而简洁。深入理解这一机制能帮助我们编写更健壮、性能更优、更易于维护的 Node.js 应用程序。
版权声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

主题网站建设将夜影院在线观看免费完整版

EmotiVoice语音合成系统容错机制与异常处理策略 在虚拟主播实时开播、游戏NPC即兴对话、智能客服情绪化应答等场景中,用户早已不再满足于“能说话”的机械音。他们期待的是有温度、有性格、甚至能共情的声音——这正是高表现力语音合成技术的战场。EmotiVoice 作为一…

张小明 2025/12/25 23:09:16 网站建设

有官网建手机网站企业网站模板源码有哪些

音频放大器TPA3116D2在零售环境广播系统中的应用在便利店、连锁超市和无人零售终端日益普及的今天,音频播报系统已不再是简单的“背景音乐播放器”,而是承担着促销信息推送、服务提醒、安全广播乃至顾客动线引导的重要交互媒介。一个清晰、稳定、高保真的…

张小明 2025/12/27 15:49:39 网站建设

做脚本从网站引流怎么创建一个属于自己的网站

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 用InsCode快速开发一个微软系统直链生成器前端界面。包含版本选择下拉框、架构选择(x86/x64)、下载按钮和实时链接显示区域。后端调用公开API获取直链,1小时内完成可部署…

张小明 2026/1/8 14:03:22 网站建设

网站建设比选文件电子商务有限公司怎么注册

常见系统备份工具的使用与特性 1. tar 工具 1.1 基本介绍 tar(tape archive)是最古老且常用的备份工具之一,它可以在文件系统的文件中创建存档,也能直接在设备上创建。与之前讨论的压缩工具类似,tar 工具接受选项来确定存档的位置和要对存档执行的操作,指定给 tar 命令…

张小明 2025/12/25 23:32:36 网站建设

一做特卖的网站企业信息系统规划的含义

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 生成一个对比传统CAN和CAN FD性能的测试程序。要求:1) 相同硬件环境下测试吞吐量;2) 错误率统计;3) 延迟测量;4) 生成可视化对比图表…

张小明 2026/1/1 7:14:57 网站建设