小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
模块化
CMJ 被熟知得益于 Node 社区的炸裂兴起,不过目前 Node 已经转移向了 ESM。不过有兴趣的依然可以研究研究,Node 中的 CMJ 实现 Node 中 CMJ 实现源代码
1、模块化如何划分?
不管是 CMJ 还是 ESM,都不约而同的选择了以「文件」为基本单位来划分模块。
文件是组织代码结构的基本单位,在模块加载器自身的处理上来说,也是一个相对来说更容易的粒度~
2、如何使用?
// a.js
exports.action = function () {
console.log('Action!');
}
// b.js
const a = require('./a.js');
a.action();
其实模块机制也是在设计 API,在设计层面,要注重考虑简洁、易用,CMJ 统一使用 require 关键字来引入模块,暴露模块的方式是挂载到 exports 对象引用上(当然也可以重写这个对象引用),这样的设计使得用户使用、学习起来没有过多的心智负担,想想一个模块加载器暴露给你的基本 API 就有十几个,你会觉得好用吗?
3、同步还是异步?
这也是设计之初就需要考虑到的问题,对模块的加载解析过程如果是异步,那必然和同步的处理方式有极大的区别,CMJ 是 之所以被 Node 采用,也是因为其设计之初考虑的就不是浏览器层面的,更偏向 Server, Node 本身在 IO 上就有足够的底气和实力,同步的方式契合了需求,只是目前推崇的 ESM 是官方的,未来的,Node 必须妥协于大流。
CommonJS不适用于浏览器端,因为它是同步的,对于浏览器不友好。
ESM
与 CMJ 的对比:
1、使用方式不同
CMJ:require and module.exports
ESM:import and export | export default
2、对基本类型,CMJ 是值拷贝,ESM 则是引用
3、动态运行时,静态编译(import 语句时静态执行,export 则是动态绑定的)
4、ESM 提升特性,import 必须写在文件最上方,不可用变量拼接路径
5、ESM 支持 Top-level await,this-undefined
6、ESM 天然支持 dynamic import,CMJ 本身则是基于运行时
7、ESM 现在被大多数现代浏览器原生支持,通过 type="module" 进行标识 (相比较于CMJ,减少了build文件的过程)
8、同步,异步
ESM将流程拆分为了三个步骤进行,首先是【构建阶段】解析模块,创建底层数据结构Module Record(可以看成是AST结构节点),然后【实例化阶段】解析import,export存入内存(这个时候代码并没执行),【执行阶段】最后才是执行 然后将执行得到的结果放进对应的内存中,这样的过程拆分为了三个主要步骤,意味着ESM拥有了CMJ不具备的异步的能力!
为啥要拆成这么几个步骤? 前端常常面临的场景是多
chunk渲染,通过入口文件<script src='index.js' type='module'/>进来,可能需要加载很多js 模块,这个时候如果ESM机制本身是多过程且可分离的, 就可以最大限度的压榨浏览器并行下载能力,快速加载依赖(当然底层支持按需更yyds),这是ES modules规范将算法分为多个阶段的原因之一
多阶段算法也有弊端,比如不能
import { foo } from "${fooPath}/a.js"这样使用,因为构建依赖图是在第一阶段,这个时候路径信息是没有的 为了解决这个问题提出了dynamic import,底层其实单独给这种情况创建了Module Record,然后通过module map的方式管理起来(module map就是一种管理Module Record的数据结构)
JS引擎会深度优先后序遍历模块树,完成实例化过程,采用动态绑定的方式来联系exportimport值,这是和CMJ非常不同的地方
三阶段设计,天生支持循环引用 没有完成三阶段的时候,会标记为
Fetching状态,循环引用的时候,看到是Fetching状态就先不管这里了,继续执行,等完成执行阶段,就会把对应的import和export链接到一个内存地址 这样就可以访问到了