了解一下ES module 和 Commonjs

2023-09-01 09:09:57 浏览数 (2)

最近测试了几个 ES module 和 Commonjs 的例子,理解了之前不太理解的概念,记录一下。要是想多了解的可以去看看阮老师的 Module 那部分。会贴一小部分的代码,不会贴所有验证的代码。

Commonjs require 大概流程

本质上 Commonjs 一直是 node 在使用的规范,虽然其他平台也可以使用。

  • 处理路径,node 有专门的 path 模块和__dirname 等,将路径转成绝对路径,定位目标文件
  • 检查缓存
  • 读取文件代码(fs)
  • 包裹一个函数并执行(自执行函数)
  • 缓存
  • 返回 module.exports
ES module 大概流程

最重要的应该是解析依赖了,ES module 如果都是同步的,会很慢。都说 ES module 是异步的,在不同环境会有不同的结果。其实 ES module 的三个步骤是可以分开异步进行。在浏览器,会使用 HTML 的规范,最后的实例化是同步,在 node 环境,文件都是在本地,同步就显得很容易。

  • 模块解析,入口文件开始,构建 Module Record,然后放置到 Module Map。Module Map 相当于每一个 js 文件,Module Record 相当于里面依赖的每一个 import
  • 获取文件,解析文件,进行 JavaScript 的解析,变量提升等
  • 实例化,执行文件内容
exports 与 module.exports

Commonjs 可以用 exports.xxx 导出,也可以用 module.exports = {}导出,因为整个文件读取之后会包裹到一个自执行函数,差不多是这样:

代码语言:javascript复制
(function(exports, require, module, filename, dirname){

})(exports, require, module, filename, dirname)

如果直接 exports = {}那么导出是无效的。下面三个例子就可以很好的理解:

代码语言:javascript复制
function fn(obj){
  obj.num = 2;
};
let obj = {
  num: 1;
};
fn(obj);
console.log(obj);


function fn(obj){
  obj = {num: 2};
};
let obj = {
  num: 1;
};
fn(obj);
console.log(obj);


function fn(obj){
  obj = 2;
};
let obj = 1;
fn(obj);
console.log(obj);

对象是指针的引用,相当于 obj = xxxx,用 obj.xx 赋值其实就是给指针 xxxx 指向的对象赋值,如果 obj = {},相当于 obj 的指针改变了,相当于 obj = xx,所以 exports = {}是无效的。

ES module 是值的引用,Commonjs 是值的拷贝

这块其实挺好实验的,导出一个变量,调用函数改变这个变量再输出,可以得到 Commonjs 的值是不会因为执行了 add 就改变,ES module 就会:

代码语言:javascript复制
let a = 10;
exports.a = a;
exports.add = () => {
  a  ;
};

let a = 10;
export const b = a;
exports.add = () => {
  a  ;
};
ES module 是编译时输出,Commonjs 是运行时加载

运行时加载也比较好实验(个人观点这样可以表示是运行时加载):

代码语言:javascript复制
main.js
let a = require('./a.js');
let b = require('./b.js');

a.js
exports.a = 'a';
let b = require('./b.js');
exports.aa = 'aa';

b.js
let a = require('./a.js');
console.log(a,'in b.js');

这样去执行的时候,b.js 里面的 a 是{a: 'a'},如果把 exports.aa = 'aa';放到 let b = require('./b.js');之前,b.js 里面的 a 是{a: 'a', aa: 'aa'}。

所以 Commonjs 是一边运行一边加载,当 a.js 执行到 let b = require('./b.js');的时候,之前的代码是执行过了,并缓存起来,这时候就会去加载 b.js 并执行。

ES module 是编译时输出

不太确定是否能这样理解:

代码语言:javascript复制
index.js
import {c} from './c.js';


c.js
import {d} from './d.js';
export let c = 'c';


d.js
import {c} from './c.js';
console.log(c,'in d.js');
export const d = 'd';

得到的结果会报错:Cannot access 'c' before initialization,如果 let c 改成 var c,结果是 undefined in d.js。

ES module 会有一个跟 JavaScript 解析一样的过程,先是解析整个 js,做一些变量提升,然后再执行。就是说会先加载所有的文件,并且解析,不会执行,在所有依赖文件加载解析完成,再开始执行。所以我是这样去理解的 ES module 是编译时输出。

ES module 和 Commonjs 循环引用的区别

这点其实挺重要的,ES module 和 Commonjs 都是通过缓存来解决循环引用的问题,不会造成死循环。Commonjs 是运行时加载,在解析到 require 的时候,会先检查缓存,如果没有,会先进行缓存再继续往下执行:

代码语言:javascript复制
main.js
require('./a.js');

a.js
let b = require('./b.js');
exports.a = 'a';
console.log('a.js', b);

b.js
let a = require('./a.js');
console.log('b.js', a);
exports.b = 'b';

result:
b.js {}
a.js { b: 'b' }

大概流程:

  • main.js require('./a.js'); 检查缓存,没有 a.js,执行 a.js
  • a.js,检查缓存,没有,缓存 a.js。执行 let b = require('./b.js');,检查缓存,没有 b.js,执行 b.js
  • b.js,检查缓存,没有 b.js,缓存 b.js。执行 let a = require('./a.js');,检查缓存,有 a.js,获取缓存,打印获取的缓存,b.js 缓存加上 b: 'b'
  • 回到 a.js,a.js 缓存加上 a: 'a',打印

所以 Commonjs 多次引入和循环引入的解决方案,是先缓存,再根据执行的内容新增缓存的内容,而且只会执行一次。

ES module 解决多次引入和循环引入也是依赖缓存,但是缓存的机制不一样。ES module 是值的引用和编译时输出,ES module 导出的是内存地址的索引:

代码语言:javascript复制
index.js
import {c} from './c.js';

c.js
import {d} from './d.js';
console.log(d,'in c.js');
export var c = 'c';

d.js
import {c} from './c.js';
console.log(c,'in d.js');
export const d = 'd';

result
undefined in d.js
d in c.js

当解析到 d.js 的 import {c} from './c.js';,会去 module map 检查是否有 c moduel record,有,建立模块指向。当依赖解析完成之后,代码也解析完成了,最后实例化运行代码,所以 d.js 执行的时候 c 是 undefined。

ES module 动态引入 import()

Commonjs 的 require 可以是动态的,也不一定要放在顶层,ES module 的 import 就必须放在最顶层。动态加载在实际应用场景是必须的,对于性能方面有非常大的提升。最典型的就是路由懒加载,如果不是有动态 import,打包出来的是一个文件,首次加载会非常慢。还有是一些条件语句决定是否加载某些文件,对性能也非常友好。

tree shaking

ES module 可以实现 tree shaking,核心就是 ES module 是编译时输出,新进行编译再执行,编译过程就能确定哪些内容是无用的,Commonjs 就无法实现,只有在执行过程中才知道哪些内容是无用的。

node 执行 ES module

如果文件后缀是.mjs(node 执行的后缀是.cjs),那么 node 会根据 ES module 规范去执行,如果是 js,那么 package.json 里面要新增"type": "module",否则会报错:

代码语言:javascript复制
Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
SyntaxError: Cannot use import statement outside a module

也可以配置 exports 做兼容,exports 优先级高于 main:

代码语言:javascript复制
"exports": {
    "import": "./src/index.js",
    "require": "./src/index.cjs"
  }
require 寻找引入的顺序

先看是否是内置包,如果是直接返回;看是否是相对路径,是就处理成可识别绝对路径,如果找不到就报错;不是内置包没有相对路径,从当前目录开始寻找 node_modules,找不到依次往上的目录寻找 node_modules,直到根目录,还找不到就报错。会先以文件名找,再依次是.js、.json、.node。

0 人点赞