跳到主要内容

认识

2023年03月05日
柏拉文
越努力,越幸运

一、认识


CommonJS 是一个模块规范,最初设计用于服务器端 JavaScript 环境(如 Node.js),后也被应用于浏览器端。CommonJS 规范的核心思想是通过同步的方式来定义和加载模块,从而使得 JavaScript 代码的组织和管理变得更加规范化。CommonJS 使用 module.exportsexports 对象来暴露模块的功能。使用 require 函数来导入和使用其他模块。

CommonJS 加载机制: 通过 require 函数同步加载,即模块加载完成后代码才会继续执行。这种方式适合服务器端环境,但在浏览器中可能会导致性能问题。CommonJS 本身约定以同步的方式进行模块加载,这种加载机制放在服务端是没问题的,一来模块都在本地,不需要进行网络 IO,二来只有服务启动时才会加载模块,而服务通常启动后会一直运行,所以对服务的性能并没有太大的影响。但如果这种加载机制放到浏览器端,会带来明显的性能问题。它会产生大量同步的模块请求,浏览器要等待响应返回后才能继续解析模块。也就是说,模块请求会造成浏览器 JS 解析过程的阻塞,导致页面加载速度缓慢。

CommonJS 加载时机: CommonJS 在代码运行时加载模块, 可以通过 require 实现动态、同步加载模块。不支持静态分析, 没有办法在编译时进行优化。因此, CommonJS 允许 require 可以在任何地方动态的加载模块。

CommonJS 缓存机制: 当模块第一次被加载时,Node.js 会将其缓存起来。后续对相同模块的 require 调用将返回缓存中的实例,而不会重新执行模块代码。这确保了模块的单例特性和一致性。

CommonJS 导出结果: 原始值(字符串、数字、布尔值) 是不可变的,CommonJS 中导出的原始值是拷贝的, 其他模块通过 require 获取的是对这个原始值的拷贝。修改模块内部的原始值不会影响到其他地方,因为其他地方获得的是原始值的拷贝。对象和函数是可变的,CommonJS 中导出的对象或函数是引用的, 其他模块通过 require 获取的是对这个对象或函数的引用。修改模块内部的对象或函数的状态会影响所有引用它的模块,因为这些模块共享同一个实例。

二、语法


2.1 基础语法

// utils.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add: add,
subtract: subtract
};
// main.js
const utils = require('./utils');
console.log(utils.add(1, 2)); // 3

2.2 缓存机制

CommonJS 缓存机制: 当模块第一次被加载时,Node.js 会将其缓存起来。后续对相同模块的 require 调用将返回缓存中的实例,而不会重新执行模块代码。这确保了模块的单例特性和一致性。

// a.js

let counter = 0;

function increment() {
counter++;
console.log(`Counter in a.js: ${counter}`);
}

module.exports = {
increment,
};
// main.js
const a = require('./a');

a.increment(); // 输出: Counter in a.js: 1

// 重新加载模块,验证缓存机制
const b = require('./a');

b.increment(); // 输出: Counter in a.js: 2

// 再次加载模块,验证缓存机制
const c = require('./a');

c.increment(); // 输出: Counter in a.js: 3

2.3 导出结果

CommonJS 导出结果: 原始值(字符串、数字、布尔值) 是不可变的,CommonJS 中导出的原始值是拷贝的, 其他模块通过 require 获取的是对这个原始值的拷贝。修改模块内部的原始值不会影响到其他地方,因为其他地方获得的是原始值的拷贝。对象和函数是可变的,CommonJS 中导出的对象或函数是引用的, 其他模块通过 require 获取的是对这个对象或函数的引用。修改模块内部的对象或函数的状态会影响所有引用它的模块,因为这些模块共享同一个实例。

导出原始值: 当前仅当 module.exports = 原始值 时,才为 CommonJS 中导出原始值

let a = 0;

module.exports = a;

导出对象: module.exports = {} 或者 modulex.exports.a = …… 都是 CommonJS 中导出对象或函数

module.exports = {
……
}

// 或者

module.exports.a = xxx;

一、验证a.jsc.js 导入 b.js, a.js 修改 b.js 中的对象属性值,c.js 是否有影响?: CommonJS 中导出的对象或函数是引用的, 其他模块通过 require 获取的是对这个对象或函数的引用。修改模块内部的对象或函数的状态会影响所有引用它的模块,因为这些模块共享同一个实例。

// b.js
let d = { value: 1 };
module.exports = d;
// a.js
const b = require('./b.js');
b.value = 10;
console.log('a.js - b.value:', b.value); // 10
// c.js
const b = require('./b.js');
console.log('c.js - b.value:', b.value); // 10

在这个例子中:

  • a.js 导入 b.js 并修改了 b.value

  • c.js 也导入了 b.js,并显示了 b.value 的修改结果。

  • 结果:a.jsc.js 都会看到 b.value 的修改,因为 d 对象是通过引用传递的。

二、验证a.jsc.js 导入 b.js, a.js 修改 b.js 中的原始值,c.js 是否有影响?: CommonJS 中导出的原始值是拷贝的, 其他模块通过 require 获取的是对这个原始值的拷贝。修改模块内部的原始值不会影响到其他地方,因为其他地方获得的是原始值的拷贝。

// b.js
let count = 1;
module.exports = count;
// a.js
const b = require('./b.js');
b += 1;
console.log('a.js - count:', b); // 2
// c.js
const b = require('./b.js');
console.log('c.js - count:', b); // 1

在这个例子中

  • a.js 导入了 b.jscount 值,并对它进行了修改(虽然原始值 count 不能直接修改,它会重新赋值)。

  • c.js 导入了 b.jscount 值,它仍然是 b.js 中的原始值。

  • 结果:a.jsc.js 看到的 count 值是不同的,因为原始值是按值传递的,a.js 的修改不会影响 c.js 的值。

三、场景


3.1 Vite

3.2 Node

3.3 Rollup

3.4 Browser

3.5 Webpack

四、问题


4.1 如何清除 require 缓存

方案: 通过 delete require.cache 清除缓存

delete require.cache[require.resolve('./server.js')];
app = require('./server.js');

4.2 为什么 CommonJS 不适用于浏览器?

CommonJS 是同步加载,也就是说一个文件的所有代码都必须等待require("依赖")执行加载完成。如果加载时间很长,整个应用就会停在哪里等。如果是服务端,所有模块都放在本地,同步加载完成的时间,仅仅是读硬盘的时间。如果是在浏览器环境,模块都放在服务端,同步加载的时间中网络延迟时间占很大一部分,这段时间浏览器都处于假死的状态。因此浏览器只能异步加载模块。

4.3 CommonJs 与 ES Modules 有什么区别?

CommonJS 使用 module.exportsexports 对象来暴露模块的功能。使用 require 函数来导入和使用其他模块。

  • CommonJS 加载机制: 通过 require 函数同步加载,即模块加载完成后代码才会继续执行。这种方式适合服务器端环境,但在浏览器中可能会导致性能问题。CommonJS 本身约定以同步的方式进行模块加载,这种加载机制放在服务端是没问题的,一来模块都在本地,不需要进行网络 IO,二来只有服务启动时才会加载模块,而服务通常启动后会一直运行,所以对服务的性能并没有太大的影响。但如果这种加载机制放到浏览器端,会带来明显的性能问题。它会产生大量同步的模块请求,浏览器要等待响应返回后才能继续解析模块。也就是说,模块请求会造成浏览器 JS 解析过程的阻塞,导致页面加载速度缓慢。

  • CommonJS 加载时机: CommonJS 在代码运行时加载模块, 可以通过 require 实现动态、同步加载模块。不支持静态分析, 没有办法在编译时进行优化。因此, CommonJS 允许 require 可以在任何地方动态的加载模块。

  • CommonJS 缓存机制: 当模块第一次被加载时,Node.js 会将其缓存起来。后续对相同模块的 require 调用将返回缓存中的实例,而不会重新执行模块代码。这确保了模块的单例特性和一致性。

  • CommonJS 导出结果: 原始值(字符串、数字、布尔值) 是不可变的,CommonJS 中导出的原始值是拷贝的, 其他模块通过 require 获取的是对这个原始值的拷贝。修改模块内部的原始值不会影响到其他地方,因为其他地方获得的是原始值的拷贝。对象和函数是可变的,CommonJS 中导出的对象或函数是引用的, 其他模块通过 require 获取的是对这个对象或函数的引用。修改模块内部的对象或函数的状态会影响所有引用它的模块,因为这些模块共享同一个实例。

EsModule 通过 export 关键字导出模块中的功能,可以是变量、函数、类等。使用 import 关键字导入其他模块的功能。

  • Es Module 加载机制: 通过 import 语句静态分析、导入, 也就是说在编译时就完成加载。也可以通过 import() 函数进行异步、动态导入,允许按需加载模块。

  • Es Module 加载时机: ES Module 是静态编译, 即编译时加载, 也就是说在编译时就完成加载, 因此在编译时就能够确定模块的依赖关系, 输入和输出变量。意味着在代码执行之前, 需要对模块结构进行分析, 所以 import 语句必须位于模块最顶层。Es Module 同样支持 import() 动态导入, 异步加载模块, 返回一个 Promise

  • Es Module 缓存机制: ES Module 使用 URL 或者相对路径作为模块标识。每个模块的加载是基于其 URL 的唯一性。当一个模块通过 import 语句被加载时,浏览器(或 JavaScript 运行时环境)会解析其 URL,并将模块内容缓存起来。这个缓存是基于模块的 URL 的。模块的缓存是通过 URL 进行标识和管理的。如果一个模块的 URL 相同,则表示它是同一个模块,浏览器会重用之前缓存的实例,而不是重新加载和执行模块代码。包括通过动态导入语法 import(),可以异步加载模块。这种动态加载也会受到缓存机制的影响,即相同的 URL 会返回缓存中的模块实例。

  • Es Module 导出结果: ES Module 模块导入的实例都是单例的, 当多个模块导入同一个模块时,所有导入都引用相同的模块实例。模块的内容(例如导出的变量、函数、对象等)在整个应用中只有一个实例。修改模块的内容(如变量、对象属性等)会影响所有引用该模块的地方,因为它们引用的是同一个实例。综上所述: ES Modules 确保了导出的模块在整个应用中只有一个实例。所有对该模块的引用都共享同一个实例,确保状态的一致性。

  • Es Module Tree shaking 是基于 ES6 模板语法(importexports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量, 进而判断哪些模块已经加载, 哪些模块和变量未被使用或者引用,进而删除对应代码。

4.4 CommonJs 与 ES Modules 可以相互引用吗?

ES Modules模块当中,是支持加载CommonJS模块 的。但是反过来,CommonJS 并不能require ES Modules 模块

4.5 为什么 Es Modules 模块比 CommonJS 模块更好?

ES ModulesESM)相较于 CommonJS 模块具有多方面的优势。这些优势主要体现在标准化、性能优化、静态分析等方面。下面详细介绍 ES Modules 的这些优点以及它们如何使 ESM 相较于 CommonJS 更好。

1. 标准化和一致性: ES ModulesJavaScript 的官方模块标准,自 ECMAScript 2015ES6)起被引入。作为官方标准,它具有以下优点:

  • 统一的语法:ESM 提供了一套统一的语法(importexport),消除了不同模块系统之间的语法不一致性。

  • 广泛支持:所有现代 JavaScript 环境(包括浏览器和 Node.js)都支持 ES Modules。随着时间的推移,ESM 成为 JavaScript 模块化的标准选择。

2. 静态分析和优化: ES Modules 支持静态分析,使得编译工具和打包工具能够进行更高效的优化:

  1. 静态结构:ESM 使用静态的 importexport 语法,这使得模块的依赖关系在编译时就可以被解析。编译工具可以在编译时确定哪些模块被导入,哪些被导出。

  2. 树摇(Tree Shaking):由于静态结构,工具可以执行树摇优化,去除未使用的代码,从而减小打包后的文件体积。

3. 异步加载: ES Modules 支持异步模块加载,提供了动态 import() 语法, 动态导入, 可以按需加载模块,从而优化应用的加载时间和性能。动态导入使得模块可以在运行时进行加载,允许更灵活的代码拆分。

4. 更好的性能: ESM 使用 URL 作为模块标识,模块加载和缓存机制更高效。每个模块只加载一次,所有导入都共享同一个实例。由于静态分析支持,现代打包工具能够对 ES Module 进行更多优化,如代码分割和按需加载,从而提高性能。

5. 更强的语法支持: ESM 允许模块同时使用命名导出和默认导出,提供了更灵活的导出方式。ESM 支持在模块顶层直接使用 await,使得异步编程更加直观。