跳到主要内容

模块

模块模式

基本概念

依赖关系
  • 使用有向图表示依赖关系;

依赖关系

加载策略
  • 同步加载: 按顺序依次加载;
    • 性能问题: 同步加载堵塞进程;
    • 复杂性: 管理加载顺序;
  • 异步加载: 按需加载, 加载后执行回调;
  • 动态加载: 运行时确定是否加载某模块, 加大静态分析难度;
静态分析
  • 检查代码结构, 在不执行代码的情况下推断其行为;
循环依赖
  • 依赖之间循环依赖;

模块模式历史历程

命名空间
  • 导出一个对象, 对象上有若干类;
  • 可配合 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 模块
  • ES6 提出;

ES6 模块

模块标签

  • 首先执行普通 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" });

模块导出

  • 一个模块可声明多个命名导出;
  • 一个模块只能有一个默认导出;
  • 可通过 as 使用别名, 转换为命名导出或默认导出;
const foo = "foo";
const bar = "bar";
const baz = "baz";

export { foo, bar as myBar, baz };
export default foo;
export { foo as default }; // 等同于 export default foo;

模块导入

// 导入默认导出
import foo from "./foo.js";
import { default as foo } from "./foo.js";
// 导入命名导出
import { bar, baz } from "./foo.js";
// 导入所有导出
import * 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 引擎执行代码;
  • 对每个模块进行求值并赋值;
  • 整个模块求值完成后才会修改至模型映射;
  • 由于存在副作用, 每个模块只运行一次;
  • 基于深度优先的后序遍历;

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模块内变量' }