第二章
AMD、CMD(了解即可)
AMD和CMD都是ES module 出现之前,浏览器端的模块化方案,而且都是社区规范而不是js官方标准,现在已经完全被ESM取代,唯一
用途就是维护老项目。
AMD(Asynchronous Module Definition)
特点:
- 异步加载依赖
- 浏览器为主
- 推崇 依赖前置
这个规范的代表实现是 ReauireJS
写法
define(['jquery', 'lodash'], function($, _) {
// 模块代码
return {}
})
特点: 浏览器异步加载,依赖一开始就全部加载执行,再执行代码
CMD ( Common Module Definition)
特点:
- 异步加载依赖
- 就近依赖
- 写法更贴合 CommonJS
- 推崇 延迟执行
这个规范的代表实现是Sea.js
写法
define(function(require, exports, module) {
var $ = reqyire('jquery')
exports.foo = function() {}
})
特点:一开始不写依赖,用的时候再加载,做到延迟加载
UMD(了解即可)
UMD是通用模块定义,是一套兼容格式,目的是为了让一个文件同时支持 AMD、CommonJS、普通浏览器全局变量,主要用于
编写第三方库,现在的vite/webpack会自动生成,不需要手动编写
IIFE(了解即可)
全称: immediately invoked function expression (立即执行函数)
为了创建一个独立作用域,避免污染全局变量而生。因为以前没有let / const /块级作用域,所以用IIFE创造一个局部作用域
写法
(function() {
var a = 10
})()
模块识别规则
Node 识别一个js文件是 ESM 还是 CommonJS ,只看两个点
- 文件后缀名(优先)
- 最近的 package.json 里的 "tyoe"字段
后缀名
.cjs 只能是 CommonJS
.mjs 只能是 ES Module
.js 看 package.json 的 type
type
从当前文件目录往上找,直到找到最近的 package.json
"type": "module"
所有的 .js 文件都是 ESModule
"type": "commonjs"
或者 没有 type字段,就是默认的 commonjs
边界
CJS
合法语法
- require()
- Module.exports / exports
- __ dirname / __ filename
- this = module.exports
可以require什么
- CommonJS 模块
- JSON / 原生模块
ESM
合法语法
- import
- export
- import()
- 顶层 await
- import.meta.url
可以import什么
- ESM模块
- CJS模块
- JSON/原生模块
规则
-
因为 CJS只支持同步,所以 一般CJS不能引用ESM,如果非得引入必须使用动态import。而ESM可以引用CJS。但需要注意,CJS的 module.exports 整体会被当成 default export,并且不支持解构命名导入
-
type 的 module 和 commonjs 都只对当前目录下的 js文件生效,不会影响子目录以外的文件
-
CJS 模块是函数包裹的作用域,而 ESM是真正的块级作用域,遵循严格模式,也就是CJS的this是 当前模块导出对象
而 ESM的this是undefined
-
require 是运行时可以写在任何地方,而import只能写在顶层
-
ESM导入必须加后缀,而CJS可以省略
补充
什么是顶层await(top-level-await)
顶层await是不用写在async函数里,直接在模块最外层写await,它会让模块异步初始化。目的是为了让整个模块变成异步加载,模块会等待await完成后,再执行后续代码,其他模块inport它时,也会等待它异步完成。
为什么CJS不支持
CJS是同步模块系统,全程同步执行,不支持暂停等待异步
import.meta.url是什么
是一个字符串,打印出来值是 当前这个模块文件本身的绝对路径URL,因为ESM里面没有 __ dirname 和 __ filename ,所以想要
获取当前文件路径,就需要import.meta.url
循环依赖处理差异
循环依赖,也就是a文件引入b,b文件引入a。比如当a要用b的函数,b又要用a的函数,互相需要,就产生了循环依赖。
因为CJS和ESM的加载机制不一样,所以处理循环依赖的方式也不一样
CJS处理方式
CJS对循环依赖处理比较简单,模块加载到哪就返回当前的导出对象,容易出现不完整导出
模块加载顺序
- 先静态扫描整个依赖树
- 给所有导出变量生成为赋值的引用
- 统一执行代码,后面赋值了,前面的引用也会同步更新
ESM 是静态解析 + 活引用绑定,会先建立引用关系再执行,循环依赖更稳定,能正确获取最终导出值
区别
CJS 导出的是值拷贝,循环依赖拿不到完整对象,ESM是只读活引用,循环依赖更安全
动态加载: import() 与 懒加载
import()
是动态加载函数,属于js语言内置的函数,其实import() 本质就是一个promise,所以才能再CJS里面加载ESM。
懒加载
懒加载是一种性能优化策略,指延迟加载资源,只有当组件需要被渲染时才去加载对应的js,减少首屏包体积,加快首屏速度
模块缓存机制
不管是CJS还是ESM 模块第一次被加载后,会把导出的结果换存起来,下次再require/import 同一个模块时,直接
拿缓存,不再重新执行模块代码。目的是为了避免重复执行、提高性能
CJS
缓存存在 require.cache 对象里,缓存key是模块的绝对路径,只要路径一致,就命中同一个缓存
模块代码只执行一次,之后都用缓存导出值
const a1 = require('./a.js')
const a2 = require('./a.js')
console.log(a1 === a2) // true
注意
模块一旦加载并缓存,就算把文件内容更新了,再次require也是旧缓存,不会自动更新,想要拿到更新内容必须先
手动清除缓存,再重新 require。这就是循环依赖的问题,它问题不是循环,而是模块执行到一半就被缓存了,导致另一个模块
拿不到完整的导出
如何清除CJS缓存
delete require.cache[require.resolve('./a.js')]
删完再 require 模块会重新执行一遍
ESM
根CJS一样第一次加载后缓存,后面再导入同一模块直接用缓存,缓存key是模块绝对路径/URL,但是CJS是直接暴露缓存对象,
而ESM不会暴露缓存对象,而且ESM也不支持清缓存,只能重启node/刷新浏览器,它实现更新的方式是使用构建工具
让构建工具监听文件变化、重新编译模块、用HMR替换旧模块导出