第二章

4 阅读5分钟

第二章

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替换旧模块导出