对CommonJS,AMD,CMD规范以及script标签异步加载的理解

对 CommonJS,AMD,CMD 规范以及 script 标签异步加载的理解

1.牢骚

CommonJS, AMD, CMD , 其实很早就接触过了。
当时, 网上的文章看得眼花缭乱, 只依稀记得几个模糊的概念。
什么依赖前置 , 什么按需加载。
一头雾水。

现在再回过头来看看概念 , 网上部分文章用词模棱两可。
给我们这些菜鸡, 带来了理解的偏差和困惑。
记得第一个项目还用了 requireJS 。
时过境迁,现在入门前端 , 都是直接上 webpack 了 。
但我觉得还是有必要理一理 。


2. 是什么

CommonJS, AMD, CMD 是规范, 理念 ;

  • 对 CommonJS 的实现 , 有 node 的模块系统 ;
  • 对 AMD 的实现有 require.js ;
  • 而 CMD, 是在 sea.js 的实现中提出来的 (但是在Google和Stack Overflow, 这个概念很少被提到, 一般出现在国内)

  • CommonJS 规范, 模块加载是同步的
    对node来说,模块存放在本地硬盘,同步加载,等待时间就是硬盘的读取时间,这个时间非常短;

  • AMD、CMD 规范,模块加载是异步的
    目的, 是为了适应浏览器环境,加载的时间取决于网络的好坏,可能要等很长时间;


3. 先说 async 和 defer

记得看文章的时候, 看到了下面这段话 ,
在这里插入图片描述
我看了半天, 总觉得不是很理解 , 为什么呢 ?
这里说, 脚本标签天生是异步的 , 那为什么会出现 async 和 defer ?
然后, 我发觉我并没有把 加载执行的概念区分清楚 , 这里的加载我把它理解为浏览器中的下载
这里贴一张图就很清楚了:
defer和async

  • <script> 标签, 在下载和执行的时候 , 会阻塞 dom 的渲染进程 , 所以如果把<script> 标签放在<head>中, 当 js 文件很大或者网络差时, 会导致页面长时间空白( 顺带提一下, <script>标签并不会阻止其他的<script>标签的下载, 现代浏览器中多个<script>下载是并行的, 在chrome中, 默认支持6个资源(http1.x)并行下载 ), 另外 , 脚本是按照<script>标签的书写顺序执行的 ;
  • <script defer> 在加上defer以后, 下载的过程就不会阻塞 dom 渲染了, 但脚本的执行是在 dom 渲染完毕之后;
  • <script async>在加上async以后, 下载的过程同样不会阻塞 dom 渲染, 但脚本会在下载完后立刻执行, 所以存在多个<script async>时, 无法保证多个 js 文件的执行顺序, 加载较快的脚本会执行;

所以 defer, async 主要作用于加载阶段, 执行阶段仍然会阻塞 dom 渲染


4. 再看 require.js 的异步体现

再看看使用 require.js 的模块写法
新建 main.js / a.js / b.js , main.js 为入口, 引用了 a.js , b.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main.js
// waitSeconds = 0的配置, 是为了防止文件过大或网络不佳时, 加载时间过长导致require报`Load timeout for modules`的错误
require.config({
waitSeconds: 0
});
require(["a.js", "b.js"], function(a, b) {
// handle / use a, b
console.log(a);
console.log(b);
});

// a.js ------------------------------
define([], function() {
return {
a: 111111111111
};
});

// b.js ------------------------------
define([], function() {
return {
b: 222222222222
};
});
  • 文件 开始下载 的 顺序: main, a, b
    为什么文件下载的顺序是 main, a, b 呢? main依赖了 a b, 不是 a b 先下载吗? 那是因为,只有 main 加载之后,才知道mian依赖了啥啊
  • 执行的 顺序 : a, b, main 或者 b, a, main
    这里体现 require.js 的异步加载。 a 和 b 的加载或者说下载是并行的, 但 a 和 b 的执行顺序不确定的 , a 和 b 先执行哪一个都无所谓 ,只需要保证回调函数在 a 和 b 都执行完之后再执行就可以了;

在 require.js 中模块加载是怎么实现的呢?
看一下 require.js 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Creates the node for the load command. Only used in browser envs.
*/
req.createNode = function(config, moduleName, url) {
var node = config.xhtml
? document.createElementNS("http://www.w3.org/1999/xhtml", "html:script")
: document.createElement("script");
node.type = config.scriptType || "text/javascript";
node.charset = "utf-8";
node.async = true;
return node;
};

这段代码, 新建了 script 标签, 并把它的 async设置为true ,

另外, 前面说 , 依赖的模块都执行完之后, 才会执行回调函数。 那怎么判断是否 所有依赖的模块 都已经执行完 ?
多个模块的情况 , 还没看懂(捂脸) , 但是单个模块的执行状态是可以监听的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
...
...
//mentioned above about not doing the 'script execute,
//then fire the script load event listener before execute
//next script' that other browsers do.
//Best hope: IE10 fixes the issues,
//and then destroys all installs of IE 6-9.
//node.attachEvent('onerror', context.onScriptError);
} else {
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);
}
node.src = url;
...
...

上面的代码可以看到, 通过 <script>标签的 onload 事件可以判断, 该脚本是否执行完毕 ;

所以, 个人理解, require.js 的异步
第一, 是指下载的异步,
第二, 还指回调机制, 依赖模块执行完之后再执行回调函数

5. AMD 和 CMD 的理解误区

现在 再来看 AMD 和 CMD 的区别, 网上的说法:

  1. AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块
  2. CMD 推崇就近依赖,只有在用到某个模块的时候再去 require

第二点 只有在用到某个模块的时候再去require, 这种说其实是带有误导性的,
看看 sea.js 的写法:

1
2
3
4
5
6
7
define(function(require, exports, module) {
console.log(123);
var a = require("a.js");
console.log(a);
var b = require("b.js");
console.log(b);
});

这里, 难道是执行到require, 才去加载/下载require的文件吗 ?
当然不是 ! 看一下 sea.js 的代码:

1
2
3
4
5
window.define = function(callback) {
var id = getCurrentJs()
var depsInit = s.parseDependencies(callback.toString())
var a = depsInit.map(item => basepath + item)
....

sea.js 把 callback 回调函数用转换成字符串, 再找出有哪些依赖, 这些依赖模块同样是预先加载的 ,
不同在于, require.js 会立刻执行依赖模块, 而 sea.js 在遇到 require 语句的时候 , 再执行依赖模块;

5. 总结

AMD 和 CMD 最大的区别是: 对依赖模块的执行时机处理不同(注意不是加载的时机)
很多人说, requireJS 是异步加载模块,SeaJS 是同步加载模块,这么说实际上是不准确的 ;
二者加载模块都是异步的 ;
只不过 AMD 依赖前置,可以方便知道依赖了哪些模块,然后马上加载 , 在加载完成后, 就会执行该模块;
而 CMD 推崇就近依赖,把模块变为字符串解析一遍, 找到依赖了哪些模块, 在加载模块完成后, 不立刻执行, 而是等到require后再执行;
上面只说了异步相关的概念, 其实 require.js / sea.js , 最重要的还是模块化
模块化降低耦合,依赖清晰,让调试, 加功能, 任务分配和交接都更方便。