概述
对于 ESModule的工作流程主要包含以下三个步骤:
- 构造(Construction) --- 找到、下载并解析所有文件为模块记录。
- 实例化(Instantiation) --- 在内存中找到位置用于存放所有的导出值,但是不用实际值来填充它们。然后让导出和导入都指向内存中的这些位置。这被称为链接(linking)。
- 评估(Evaluation) --- 运行代码以真实值填充这些位置。
下面我们结合源码和构建工具处理的逻辑,来理解这三个步骤
构造
这个过程涉及找到,并下载必要的模块文件。一旦找到这些文件,JavaScript 会解析这些文件并将它们转换为模块记录。模块记录是包含有关模块所有导出、导入以及源代码的信息的数据结构。
javaScript
// 导入模块
import { a } from './test.js';
实例化
在这个阶段,JavaScript 引擎会为所有导出的变量找到内存位置,并且原始的导入和导出声明都会被替换为这些内存位置的引用。这个过程称为链接,是实现模块间的交互的关键步骤。
下面我们通过源码来理解:CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
CommonJS
对于 CommonJS,具体可以看我之前的文章深入了解 CommonJs 如何在浏览器运行
CommonJS源码:
javaScript
// index.js
const { a } = require('./test')
console.log(a)
setTimeout(() => {
console.log(a)
}, 500)
// test.js
let a = 0
setTimeout(() => {
a = 1
}, 500)
module.exports = {
a: a
}
CommonJS 构建后简化代码
javaScript
let a = 0
const module = {
exports: {
a
},
};
function test(module, exports) {
console.log(exports.a)
setTimeout(() => {
console.log(exports.a)
}, 500)
}
setTimeout(() => {
a = 1
}, 500)
test(module, module.exports)
// 打印结果: 0 0
ESModule
ESmodule规范实现跟上面一样逻辑的代码
javaScript
// index.js
import a from './test'
console.log(a)
setTimeout(() => {
console.log(a)
}, 500)
// test.js
export let a = 0
setTimeout(() => {
a = 1
}, 500)
ESModule 构建后简化代码
javaScript
__webpack_require__ = {}
// 判断是否已经有该私有属性
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
// 输出值的引用的关键实现
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key], // 通过get方法巧妙返回值的引用
});
}
}
}
let a = 0
const module = {
exports: {},
};
__webpack_require__.d(module.exports, {a: () => (a)})
function test(module, exports) {
console.log(exports.a)
setTimeout(() => {
console.log(exports.a)
}, 500)
}
setTimeout(() => {
a = 1
}, 500)
test(module, module.exports)
// 打印结果: 0 1
defineProperty
可以对比打印结果,我们可以感受到值的拷贝和值的引用的区别,而对于值的引用的关键实现就是:
javaScript
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key], // 通过get方法巧妙返回值的引用
});
使用Object.defineProperty
能提供更高层次的控制,可以控制属性是否可枚举、可写、可配置,并且可以提供getter和setter方法,使得在访问和设置属性时可以执行自定义的行为。但是,如果不需要这些额外的控制,那么使用普通的赋值方式(exports[key]=definition[key])会更简单、清晰。
这里我们get: definition[key]
来实现值的引用
评估
在这个过程中,JavaScript 引擎会实际运行模块的代码。所有的实际值(如函数和变量)都会被保存在之前实例化阶段建立的内存位置中。
tree-shaking
ESModule 尽量静态化处理,特点:
- import 只能作为模块
顶层
的语句出现 - import 的模块名是静态
固定的
- import
绑定
是不变
的
所以模块依赖关系是确认的,和运行时的状态无关,所以在运行前可以进行静态分析,删除无用代码,使得最终的打包文件更小
不过要注意,tree-shaking
不会影响到那些有副作用
的模块。即使模块没有被明确导入,如果它在顶层范围有副作用(例如调用一个函数,修改一个全局变量),那么它就不会被剔除。因为这些副作用可能会影响到程序的运行。
- 正常情况下:
javaScript
// index.js
import { a } from "./test.js";
console.log(a);
// test.js
export let a = 1;
export let b = 3;
// 打包产物
(() => {
"use strict";
console.log(1);
})();
- 增加有副作用的模块
javaScript
// index.js
import { a } from "./test.js";
console.log(a);
// test.js
export let a = 1;
export let b = 3;
setTimeout(() => {
b = 2;
}, 500);
// 打包产物
(() => {
"use strict";
let e = 3;
setTimeout(() => {
e = 2;
}, 500),
console.log(1);
})();
动态引入 import()
前面讲到 import 模块名是固定的,绑定也是不变的。所以下面的代码会报错:
JavaScript
// 报错
if (a === 1) {
import module from './module';
}
上面代码中,引擎处理 import 语句是在编译时,这时不会去分析或执行 if 语句,所以 import 语句放在 if 代码块之中毫无意义,因此会报句法错误,而不是执行时错误.
ES2020 提案 引入 import()函数,支持动态加载模块
JavaScript
if (a === 1) {
import('./module').then((module) => {
// todo module
})
}
而对于动态 import()的引入,如不会影响其他模块静态解析,而对于这部分动态模块的引入,打包工具主要做了以下事项:
- 将异步模块单独打包到一个文件
- 当运行到对应代码时,在 Promise 内创建一个 script 标签加载异步文件
- 监听 script 的 onload、onerror,进行加载成功或者失败时的处理
参考文献
hacks.mozilla.org/2018/03/es-... es6.ruanyifeng.com/#docs/modul...