# CommonJS
# 核心函数
- exports
exports
是一个 object,可以在它身上添加需要导出的属性,是默认的根本导出的对象 - module.exports
module.exports
是被模块直接导出的对象,它实际上默认是exports
的引用 - require
require
实际上是会执行要导出的模块里的 code,然后导入被导入模块的module.exports
# 加载模式
commonJS 的加载模式是运行时的,同步的,它是当代码运行到 require
函数的这一行时,才会开始加载模块,并且执行模块里面的代码,模块里的代码执行完毕才会执行下一行。
# 加载模式带来的特点
- 基于这个运行时的机制,require 才可以在任何地方使用,并且在路径里面可以使用变量,因为此时变量已经有了值
- 基于这个同步的机制,当 require 的模块还在加载中时,导入的变量的值会是
undefined
(常见于循环引用中,比如 b 依赖于 a, 而 a 又没执行完毕, 那么require A
的值则会是undefined
)
# CommonJS 对循环依赖的处理
// index.js | |
let count = require("./counter.js").count; | |
console.log(count); | |
exports.message = "hello world!"; | |
// counter.js | |
let message = require("./index.js").message; | |
exports.count = 5; | |
setTimeout(() => { | |
console.log(message); | |
}, 0); |
打印的结果为: 5 undefined
原因:
- 首先加载
index.js
,执行到第一行的时候遇到 require,开始加载counter.js
counter.js
的第一行被执行,又遇到 require,需要去加载index.js
,但此时index.js
还没有加载完毕, 所以此时 message 的值为 undefined- count 变量被导出
- 注册定时器宏任务,打印 message(值为 undefined)
- 回到
index.js
- 打印 count(值为 5)
- 导出 message(值为 ‘hello world’), 然而已经没用了,因为
counter.js
里面的 message 的值已经被解析为了 undefined - 执行定时器宏任务,打印 message(值为 undefined)
# ESModule
# 加载模式
ESModule 的加载模式是分为构建阶段,实例化阶段,执行阶段的, 并且所有模块都是异步递归加载解析的。而对依赖关系的处理是在构建阶段,也就是解析时进行
- 构建阶段
主要是浏览器尝试下载所有需要的模块文件,并形成模块记录的过程
- 浏览器开始解析入口模块文件(是解析,不是执行,只是为了确定依赖),确定入口文件的依赖模块,从而发送请求,下载相关依赖模块。等到依赖模块文件返回后,浏览器继续解析这些依赖模块文件,从而确定依赖文件的依赖,然后发请求下载,并解析,如此循环递归,直到所有依赖文件全部被下载。
- 每个模块加载完毕后,都会创建相应的模块记录,同时浏览器还会维护一张模块映射表,它保存了模块路径 -- 模块记录的映射关系。
- 实例化阶段
主要是将模块里面的 export 和 import 和内存建立起关系, 也就是为 export 开辟内存空间。
- 实现 JS 引擎慧创建一个模块环境记录,用来管理模块中导入和导出的变量
- 首先处理 export, 为每一个 export 在内存中开辟一个对应的空间,但是这个时候内存空间里面没有值,赋值操作是在执行阶段发生的。
- JS 引擎处理完模块所有的导出之后,才会开始处理模块的导入,对同一个模块的导入和导出指向的是内存中的同一片内存空间
- 执行阶段
- 开始执行 js 代码,进行赋值操作,将变量的值填充到对应的内存空间
# 加载模式带来的特点
- ESModule 对模块的下载和解析是发生在构建阶段的时候的,这个时候 js 代码还没执行,变量还没值(js 引擎连内存空间都还没给它开辟),所以 import 语句不能携带变量,不能直接在代码块中嵌入
# ESModule 对循环依赖的处理
下面让我们来看一个循环依赖的例子,并分析和处理。
// index.js | |
import { count } from "./counter.js"; | |
const message = "666"; | |
console.log(count); | |
export { message }; | |
// counter.js | |
import { message } from "./index.js"; | |
const count = 5; | |
setTimeout(() => { | |
console.log(message); | |
}, 0); | |
export { count }; |
打印的结果为:5 666
原因:
- 构建阶段
- 浏览器下载
index.js
文件 - 下载完毕后,开始解析
index.js
文件,发现index.js
依赖counter.js
- 下载
counter.js
- 下载完毕后,解析
counter.js
文件,发现没有依赖 - 构建阶段完毕
- 浏览器下载
- 实例化阶段
- 为
index.js
和counter.js
的export
在内存中开辟一段内存空间 - 将
import
指向其对应的模块的export
的内存空间
- 为
- 执行阶段
- 执行
index.js
中的代码,将局部变量 message 赋值为 666(此时 message 还没有被写入到 export 的那片内存) - 打印导入的 count,但是此时 import 指向的那片内存,count 还没有被填充,所以开始执行
counter.js
的代码。 - 将局部变量 count 赋值为 5(此时 count 还没有被写入到 export 的那片内存)
- 将定时器宏任务加入消息队列
- 执行
export
来将 count 填充到内存空间 - 继续执行
index.js
,此时 import 指向的内存空间(就是 counter.js 的 export 的内存空间)count 已经被填充,顺利打印出 5. - 执行
export
来将 message 填充到内存空间 - 执行计时器宏任务中的 callback,此时
counter.js
的import
指向的内存空间(就是 index.js 的 export 的内存空间)message 已经被填充,顺利答应出 666
- 执行