前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >了解一下ES module 和 Commonjs

了解一下ES module 和 Commonjs

作者头像
wade
发布2023-09-01 09:09:57
1750
发布2023-09-01 09:09:57
举报
文章被收录于专栏:coding个人笔记coding个人笔记

最近测试了几个 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。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-05-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 coding个人笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Commonjs require 大概流程
  • ES module 大概流程
  • exports 与 module.exports
  • ES module 是值的引用,Commonjs 是值的拷贝
  • ES module 是编译时输出,Commonjs 是运行时加载
  • ES module 是编译时输出
  • ES module 和 Commonjs 循环引用的区别
  • ES module 动态引入 import()
  • tree shaking
  • node 执行 ES module
  • require 寻找引入的顺序
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档