模块
模块模式
基本概念
依赖关系
- 使用有向图表示依赖关系;
加载策略
- 同步加载:按顺序依次加载;
- 性能问题:同步加载堵塞进程;
- 复杂性:管理加载顺序;
- 异步加载:按需加载,加载后执行回调;
- 动态加载:运行时确定是否加载某模块,加大静态分析难度;
静态分析
- 检查代码结构,在不执行代码的情况下推断其行为;
循环依赖
- 依赖之间循环依赖;
ES6 之前的模块加载器
命名空间
- 导出一个对象,对象上有若干类;
- 可配合 IIFE 实现静态方法类;
Commonjs
// 加载模块
var moduleB = require('./moduleB');
// 导出模块
module.exports = {
stuff: moduleB.doStuff();
};
AMD
define("moduleA", ["require", "exports"], function (require, exports) {
var moduleB = require("moduleB");
exports.stuff = moduleB.doStuff();
});
UMD
- 统一 Commonjs 和 AMD
ES6 模块
模块标签
定义模块标签
<script type="module">
// 模块代码
</script>
<script type="module" src="path/to/myModule.js"></script>
执行顺序
- 首先执行普通 script 标签;
- 其次执行模块标签;
- 同类标签根据代码顺序同步执行;
<!-- 第二个执行 -->
<script type="module">
// 模块代码
</script>
<!-- 第三个执行 -->
<script type="module" src="path/to/myModule.js"></script>
<!-- 第一个执行 -->
<script></script>
工作者模块
// 第二个参数默认为{ type: 'classic' }
const scriptWorker = new Worker("scriptWorker.js");
const moduleWorker = new Worker("moduleWorker.js", { type: "module" });
模块导出
命名导出
export const foo = "foo";
// 别名
export { foo as myFoo };
// 一个模块可声明多个命名导出
const foo = "foo";
const bar = "bar";
const baz = "baz";
export { foo, bar as myBar, baz };
默认导出
// 一个模块只能有一个默认导出
const foo = "foo";
export default foo;
// default 别名关键字
export { foo as default }; // 等同于 export default foo;
命名导出和默认导出
// 支持同时使用两种导出
const foo = "foo";
const bar = "bar";
export { foo as default, bar };
模块导入
命名导出的导入
// foo.js
const foo = "foo",
bar = "bar",
baz = "baz";
export { foo, bar, baz };
// * 导入所有
import * as Foo from "./foo.js";
console.log(Foo.foo); // foo
console.log(Foo.bar); // bar
console.log(Foo.baz); // baz
// 自定义导入
import { foo, bar, baz as myBaz } from "./foo.js";
console.log(foo); // foo
console.log(bar); // bar
console.log(myBaz); // baz
默认导出的导入
// 等效
import { default as foo } from "./foo.js";
import foo from "./foo.js";
命名导出和默认导出的导入
// 等效
import foo, { bar, baz } from "./foo.js";
import { default as foo, bar, baz } from "./foo.js";
import foo, * as Foo from "./foo.js";
模块转移导出
// 导出所有
export * from "./foo.js";
// 使用别名
export { foo, bar as myBar } from "./foo.js";
// 重用默认导出
export { default } from "./foo.js";
// 命名导出转换为默认导出
export { foo as default } from "./foo.js";
ESM
基本概念
关系依赖图
- 不同模块之间的依赖关系;
模块记录
- 将导入的依赖文件解析成特定的数据结构;
模块实例
- 模块记录的具体内容;
- 组成部分;
- 模块代码:记录模块中的源码;
- 模块状态:记录模块中变量,函数。。。;
模块映射 (module map)
- 模块加载器缓存模块实例;
- 使用模块映射管理模块实例的缓存;
- 模块实例对应一个 URL;
异步加载
- 构建,实例化和求值三个阶段分别完成;
- ESM 模块规划文件解析,实例化和求值使用异步机制处理;
- 但未规定文件获取的机制,具体机制根据不同环境下的模块文件加载器而定;
构建
具体任务
- 获取并加载模块文件;
- 解析模块文件并创建模块记录;
获取文件
- 通过 script 或者手动指定获取入口文件;
- 根据入口文件的 import 语句获取模块依赖文件;
- 深度优先的后序遍历,根据 import 语句不断获取模块依赖文件;
解析
- 将模块文件解析为模块记录;
- 模块记录创建后,立刻添加至模块映射;
实例化
具体任务
- 为变量,函数分配内存空间;
主要过程
- js 引擎创建模块环境记录管理模块记录中的变量;
- 将变量绑定到对应的内存空间地址;
- 仅绑定地址但不初始化;
- 函数例外,函数会进行初始化;
- 基于深度优先的后序遍历遍历模块记录;
求值
- JS 引擎执行代码;
- 对每个模块进行求值并赋值;
- 整个模块求值完成后才会修改至模型映射;
- 由于存在副作用,每个模块只运行一次;
- 基于深度优先的后序遍历;
ESM 进阶
导出变量和导入变量
- 导入模块导入导出变量的内存地址;
- 导出模块与导入模块中的变量,指向了同一内存地址;
- 导出模块中对应变量的更改,会更新所有导入模块中的变量;
ESM 动态 import
- import() 运行时导入模块,作为单独的入口文件处理;
- 返回一个 await 对象;
- 该过程是可嵌套的,即动态导入的文件可以进行另外的动态导入;
- 实现按需加载和代码分割;
- 浏览器会通过 http 请求实现;
const module = await import(modulePath);
module.fn();
实例详解
- 构建阶段生成模块记录并构建模块映射;
- 实例化阶段为变量分配内存空间;
- 求值阶段;
- 执行 index.js 文件;
- index.js 导入 a.js,执行 a.js;
- a.js 导入 b.js,执行 b.js;
- 执行
let b = "原始值-b模块内变量"
,初始化 b 变量; - 执行
console.log("b模块引用a模块: ", a);
;- 此时 a 仅分配内存地址认为初始化;
- 故 a 为 uninitialized;
- 执行
let b = "修改值-b模块内变量"
,修改 b 变量; - 导出
{b}
; - 返回 a.js;
- 执行
let a = "原始值-a模块内变量";
,初始化 a 变量; - 执行
console.log("a模块引用b模块: ", b);
;- 此时
{b}
已经初始化,打印{b}
;
- 此时
- 执行
a = "修改值-a模块内变量";
,修改 a 变量; - 导出
{a}
; - 返回 index.js
- 执行
console.log("入口模块引用a模块: ", a);
;- 此时
{a}
已经初始化,打印{a}
;
- 此时
// index.js
import * as a from "./a.js";
console.log("入口模块引用a模块: ", a);
// a.js
import * as b from "./b.js";
let a = "原始值-a模块内变量";
console.log("a模块引用b模块: ", b);
a = "修改值-a模块内变量";
export { a };
// b.js
import * as a from "./a.js";
let b = "原始值-b模块内变量";
console.log("b模块引用a模块: ", a);
b = "修改值-b模块内变量";
export { b };
// 执行结果
// b模块引用a模块: [Module: null prototype] { a: <uninitialized> }
// a模块引用b模块: [Module: null prototype] { b: '修改值-b模块内变量' }
// 入口模块引用a模块: [Module: null prototype] { a: '修改值-a模块内变量' }
commonJS
同步加载
- commonJS 用于 node 端;
- 文件加载速度远快于 esm;
- 模块加载使用同步机制;
- 一次性进行构建,实例化和求值三个极端;
加载机制
- commonJS 深度优先的后序遍历;
- 在查找下一个模块之前,执行模块代码 (直到 require 语句),并实时缓存该模块;
导出变量和导入变量
- 导入模块对导出变量进行值拷贝;
- 导入模块中的变量是导出模块中的副本;
- 导出模块中对应变量的更改,不会更新导入模块中的变量;
实例详解
- 执行 index.js 文件,缓存该模块;
- 加载 a.js,进入 a.js;
- 执行 a.js,缓存该模块;
- 执行
exports.a = "原始值-a模块内变量";
;- 初始化 a 并添加至缓存;
- 加载 b.js,进入 b.js;
- 执行 b.js,缓存该模块;
- 执行
exports.b = "原始值-b模块内变量";
;- 初始化 b 并添加至缓存;
- 加载 a.js;
- 此时 a.js 已经被缓存,读取 a.js 的缓存,对 a 进行值拷贝;
- 执行
console.log("b模块引用a模块", a);
,打印 a; - 执行
exports.b = "修改值-b模块内变量";
;- 更新 b 模块缓存中的 b;
- 返回 a.js;
- 执行
console.log("a模块引用b模块: ", b);
,打印 b; - 执行
exports.a = "修改值-a模块内变量";
;- 更新 a 模块缓存中的 a;
- 返回 index.js;
- 执行
console.log("入口模块引用a模块: ", a);
,打印 a;
//index.js
var a = require("./a");
console.log("入口模块引用a模块: ", a);
// a.js
exports.a = "原始值-a模块内变量";
var b = require("./b");
console.log("a模块引用b模块: ", b);
exports.a = "修改值-a模块内变量";
// b.js
exports.b = "原始值-b模块内变量";
var a = require("./a");
console.log("b模块引用a模块", a);
exports.b = "修改值-b模块内变量";
// 输出结果
// b模块引用a模块 { a: '原始值-a模块内变量' }
// a模块引用b模块: { b: '修改值-b模块内变量' }
// 入口模块引用a模块: { a: '修改值-a模块内变量' }