认识
一、认识
CommonJS
是一个模块规范,最初设计用于服务器端 JavaScript
环境(如 Node.js
),后也被应用于浏览器端。CommonJS
规范的核心思想是通过同步的方式来定义和加载模块,从而使得 JavaScript
代码的组织和管理变得更加规范化。CommonJS
使用 module.exports
或 exports
对象来暴露模块的功能。使用 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.js
、c.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.js
和c.js
都会看到b.value
的修改,因为d
对象是通过引用传递的。
二、验证a.js
、c.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.js
的count
值,并对它进行了修改(虽然原始值count
不能直接修改,它会重新赋值)。 -
c.js
导入了b.js
的count
值,它仍然是b.js
中的原始值。 -
结果:
a.js
和c.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.exports
或 exports
对象来暴露模块的功能。使用 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
模板语法(import
与exports
),主要是借助ES6
模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量, 进而判断哪些模块已经加载, 哪些模块和变量未被使用或者引用,进而删除对应代码。
4.4 CommonJs 与 ES Modules 可以相互引用吗?
在ES Modules
模块当中,是支持加载CommonJS
模块 的。但是反过来,CommonJS
并不能require
ES Modules
模块
4.5 为什么 Es Modules 模块比 CommonJS 模块更好?
ES Modules
(ESM
)相较于 CommonJS
模块具有多方面的优势。这些优势主要体现在标准化、性能优化、静态分析等方面。下面详细介绍 ES Modules
的这些优点以及它们如何使 ESM
相较于 CommonJS
更好。
1. 标准化和一致性: ES Modules
是 JavaScript
的官方模块标准,自 ECMAScript 2015
(ES6
)起被引入。作为官方标准,它具有以下优点:
-
统一的语法:
ESM
提供了一套统一的语法(import
和export
),消除了不同模块系统之间的语法不一致性。 -
广泛支持:所有现代
JavaScript
环境(包括浏览器和Node.js
)都支持ES Modules
。随着时间的推移,ESM
成为JavaScript
模块化的标准选择。
2. 静态分析和优化: ES Modules
支持静态分析,使得编译工具和打包工具能够进行更高效的优化:
-
静态结构:
ESM
使用静态的import
和export
语法,这使得模块的依赖关系在编译时就可以被解析。编译工具可以在编译时确定哪些模块被导入,哪些被导出。 -
树摇(
Tree Shaking
):由于静态结构,工具可以执行树摇优化,去除未使用的代码,从而减小打包后的文件体积。
3. 异步加载: ES Modules
支持异步模块加载,提供了动态 import()
语法, 动态导入, 可以按需加载模块,从而优化应用的加载时间和性能。动态导入使得模块可以在运行时进行加载,允许更灵活的代码拆分。
4. 更好的性能: ESM
使用 URL
作为模块标识,模块加载和缓存机制更高效。每个模块只加载一次,所有导入都共享同一个实例。由于静态分析支持,现代打包工具能够对 ES Module
进行更多优化,如代码分割和按需加载,从而提高性能。
5. 更强的语法支持: ESM
允许模块同时使用命名导出和默认导出,提供了更灵活的导出方式。ESM
支持在模块顶层直接使用 await
,使得异步编程更加直观。