JavaScript 模块化的发展历程
任何模块化,都必须考虑的两个问题就是导入依赖和导出接口。
- CommonJS
CommonJS 规范规定,每个模块内部有两个变量可以使用,require
和module
。- require 用来加载某个模块
- module 代表当前模块,是一个对象,保存了当前模块的信息。
exports 是 module 上的一个属性,保存了当前模块要导出的接口或者变量,使用 require 加载的某个模块获取到的值就是那个模块使用 exports 导出的值
- AMD
- AMD(Asynchronous Module Definition),即 异步模块定义。
- 浏览器不能兼容CommonJS,于是AMD就出现了。浏览器不能兼容CommonJS的根本原因在于缺少node.JS的四个环境变量:module、exports、requrie、global。
- AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
首先要在 html 文件中引入 require.js 工具库,就是这个库提供了定义模块、加载模块等功能。它提供了一个全局的 define 函数用来定义模块。所以在引入 require.js 文件后,再引入的其它文件,都可以使用 define 来定义模块。
- CMD
CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。
Sea.js 是阿里的玉伯写的。它的诞生在 RequireJS 之后,玉伯觉得 AMD 规范是异步的,模块的组织形式不够自然和直观。于是他在追求能像 CommonJS 那样的书写形式。于是就有了 CMD 。 - UMD
-
UMD(Universal Module Definition),即 通用模块定义。
-
UMD 是AMD 和 CommonJS的糅合。
- AMD 模块以浏览器第一的原则发展,异步加载模块。CommonJS 模块以服务器第一原则发展,选择同步加载。它的模块无需包装(unwrapped modules)。 这迫使人们又想出另一个更通用的模式 UMD(Universal Module Definition),实现
跨平台
的解决方案。 - UMD 先判断是否支持 Node.js 的模块(exports)是否存在,存在则使用 Node.js 模块模式。再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块。
- AMD 模块以浏览器第一的原则发展,异步加载模块。CommonJS 模块以服务器第一原则发展,选择同步加载。它的模块无需包装(unwrapped modules)。 这迫使人们又想出另一个更通用的模式 UMD(Universal Module Definition),实现
-
- ES6 模块
模块功能主要由两个命令构成:export 和 import。export 命令用于导出模块的对外接口,import 命令用于导入其他模块导出的内容。
规范名称 | 服务于x端 | 方式 | 执行时机 | 同步异步 | 模块输出 | 模块顶层的this指向| |
---|---|---|---|---|---|---|
CommonJS | 服务端 | - | - | 同步/运行时加载 | 值拷贝 | this指向当前模块 |
AMD | 浏览器端 | 依赖前置 | 提前执行(加载完模块后立即执行) | 异步加载模块 | ||
CMD | 浏览器端 | 依赖就近 | 延迟执行(加载完模块不立即执行,只是加载,等到需要的时候才会执行) | 异步加载模块 | ||
ES6 Module | 浏览器端&服务端 | 静态编译(在编译的时候就能确定依赖,编译的时候输出接口) | 编译时加载块 | 值引用 | this指向undefined |
模块依赖环
ES6、CommonJS循环引用问题
什么是循环引用?循环加载指的是a脚本的执行依赖b脚本,b脚本的执行依赖a脚本。
① CommonJS模块是加载时执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。
② ES6模块对导出模块,变量,对象是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用
解决
1.在循环依赖的每个模块中先导出自身,然后再导入其他模块
这种解决办法可行的原因是, JavaScript 是一门解释型的语言,在 require 其他模块之前,已经把自身需要导出的部分都导出了,所以即便有模块载入缓存,也不影响最终结果按预期进行。
//a.js
module.exports = {
A: 'this is a Object'
};
let b = require('./B');
//b.js
module.exports = {
B: 'this is b Object'
};
let a = require('./A');
- 把重来一遍变成中断
第二次读取 a.js 的时候 Nodejs 该怎么处理呢, 如果像浏览器一样的话, 应该重头再解析一遍, 又读到 require b.js 跑进去再重来。Nodejs 称为 unfinished copy,第二进入 a.js 模块的时候, 从require b.js 的后面继续往下读取
, 这样就将环解开又回到了原先串行的解析方式, 代价就是你得知道 require 前后都写了什么, 尤其是涉及 exports 出去的值, 因为执行顺序问题, 两次的值并不相同。
不过如果我们不想中断, 就想从头到尾读呢? - 模块解析和模块加载分开处理
那我们就需要将模块解析和模块加载分开处理
, 比如我们 ES6 Module 带来的 import。因为代码的执行分成了两个阶段, 意味着无论你 import 写在哪都会比其他代码先被读取, 这也就解决了 require 前后代码执行顺序导致 exports 值不一致的问题。
通过解析加载分离, 我们就可以先解析模块的依赖关系, 而避免去读取实际的模块代码。在 a.js 中我们读取到 import 'b.js', 这时候我们可以给 b.js 添加一个 State, 并将其值设为 'Linked', 表示 b.js 已经被读取了, 然后从 b.js 读取到 import a.js, 我们再将 a.js 的 State 设为 'Linked', 然后又回到 a.js, 发现 b.js 已经是 Linked 了, 跳过, 继续读取 c.js ,通过状态标记就避免了循环依赖
。
//a.js
import b from 'b.js'
import c from 'c.js'
//b.js
import a from 'a.js'
相关文章
网友评论