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 ?
然后, 我发觉我并没有把 加载
和执行
的概念区分清楚 , 这里的加载我把它理解为浏览器中的下载
这里贴一张图就很清楚了:
-
<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
// 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的源码:
/**
* 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
,
另外, 前面说 , 依赖的模块都执行完之后, 才会执行回调函数。 那怎么判断是否 所有依赖的模块 都已经执行完 ?
多个模块的情况 , 还没看懂(捂脸) , 但是单个模块的执行状态是可以监听的:
...
...
...
//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 的区别, 网上的说法:
- AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
- CMD推崇就近依赖,只有在用到某个模块的时候再去require
第二点 只有在用到某个模块的时候再去require
, 这种说其实是带有误导性的,
看看sea.js的写法:
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的代码:
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 , 最重要的还是模块化。
模块化降低耦合,依赖清晰,让调试, 加功能, 任务分配和交接都更方便。