对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

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

  • <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 的区别, 网上的说法:

  1. AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
  2. 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 , 最重要的还是模块化
模块化降低耦合,依赖清晰,让调试, 加功能, 任务分配和交接都更方便。

上一篇:任何IDE / IDE插件是否都支持JavaScript CommonJS模块?


下一篇:javascript – system.stdout和system.stdin在casperjs中是未定义的