前端模块化之AMD与CMD原理(附源码)

栏目: JavaScript · 发布时间: 7年前

内容简介:可能现在初入前端的同学们,都直接就上手webpack了,而在几年前,没有现在这些丰富的工具,还是jquery打天下的时候,不借助node或程序却让不同js文件之间互相引用、模块化开发,确实是一件痛苦的事情。。。接下来会介绍两个有名的工具AMD(require.js)和CMD(sea.js),虽然已基本不用了,但是前端们还是需要知道以前是怎么写代码的。。。如上图,一开始前端的内容不多,主要是页面和简单的交互,所以前端的开发环境就是纯几个静态文件(html、css、js)。而html文件这样子引入js文件,有很

可能现在初入前端的同学们,都直接就上手webpack了,而在几年前,没有现在这些丰富的工具,还是jquery打天下的时候,不借助node或程序却让不同js文件之间互相引用、模块化开发,确实是一件痛苦的事情。。。

接下来会介绍两个有名的工具AMD(require.js)和CMD(sea.js),虽然已基本不用了,但是前端们还是需要知道以前是怎么写代码的。。。

2. 需求的产生

前端模块化之AMD与CMD原理(附源码)

如上图,一开始前端的内容不多,主要是页面和简单的交互,所以前端的开发环境就是纯几个静态文件(html、css、js)。而html文件这样子引入js文件,有很多缺点:

  1. 必须按顺序引入,如果 1.js中要用到jquery,那就将jquery.js放到1.js上方。
  2. 同步加载各个js,只有1.js加载并执行完,才去加载2.js。
  3. 各个js文件可能会有多个window全局变量的创建,污染。
  4. ......还有很多缺点

总之,上面的结构,在前端内容越来越多,尤其ajax的趋势、前后端分离、越来越注重前端体验,js文件越来越多且互相引用更复杂的情况下,真心乱套了,所以需要有一个新的模块化工具。

而模块化希望的样子:

html文件中

前端模块化之AMD与CMD原理(附源码)

1.js文件中

前端模块化之AMD与CMD原理(附源码)

2.js中

前端模块化之AMD与CMD原理(附源码)

我们当然是希望这样,improt、export就行了,但遗憾的是,这是es6配合node的用法,而以前的AMD、CMD可不是这么实现的。

3. AMD

即Asynchronous Module Definition,中文名是 异步模块定义 的意思。它是一个模块化开发的规范,是一种思路,是一个概念。我们不用过于纠结它到底是个啥,只需要知道它是一个规范概念,而大名鼎鼎的require.js,是它的一个具体的实现,以前很多都用这个 工具 开发,接下来我们就研究一下require.js。

require.js的用法

这里只介绍大概简单的用法,没用过的同学最好去看教程。

login.html中

前端模块化之AMD与CMD原理(附源码)

引入了require.js文件,然后指定了入口文件login.js

~~

入口文件login.js中

前端模块化之AMD与CMD原理(附源码)

loginModule.js中

前端模块化之AMD与CMD原理(附源码)

loginCtrl.js中

前端模块化之AMD与CMD原理(附源码)

注意:

  1. 实际上就是定义了两个全局变量函数,一个require(),一个define()

  2. 其实这两个函数功能和原理差不多,你可以认为他俩除了名字不一样,其他都差不多。

  3. define函数的第一个参数是个数组,写的是所依赖的模块;第二个参数是回调函数,回调函数里的参数对应的是依赖数组里的模块返回值,如:

    es6 :

    import A from 'a.js'

    import B from 'b.js'

    require.js:

    define(['a.js', 'b.js'], function(A, B) {

    })

  4. 从几个图中可以看到依赖的顺序,所以加载js文件的顺序应该是:(1)other.js (2)loginCtrl.js (3)loginModule.js (4)login.js。所以运行时,浏览器加载js的顺序也是这个样子的,不过,应该在最前面加上(0)require.js当然。

最注意!!!:

无论是CMD还是AMD,都只是让开发者写代码时变爽了,而对于浏览器来说,没啥太大变化,该加载多少js文件,js文件的顺序,都和以前没啥区别!!!

4. require.js 原理

到这里我们大概知道了,这两个工具的意义,是让开发者不再需要写一堆 <script> 标签 引入js了,让编码更爽。。。但是对于浏览器那边,没啥大的变化。

以前:

编码:

前端模块化之AMD与CMD原理(附源码)

浏览器:

前端模块化之AMD与CMD原理(附源码)

require.js后

编码:

前端模块化之AMD与CMD原理(附源码)

浏览器:

前端模块化之AMD与CMD原理(附源码)

所以结论:require.js只是采取了某种“方法”,让你在写代码时只写一个<script>,运行html文档时却是多个<script>

分析实现原理:

可能1:

编码时的html文件和运行时的html文件是两个文件,即通过某些工具复制并修改了html。可惜修改文件需要服务端程序去做,而require.js只是个js文件,所以不是这个原理。这在node下webpack可以轻松的实现。

可能2:

既然可能1是不对的,那么说明了,浏览器运行的html文件和编码时的html文件是一模一样的。所以只剩下第二条路了,就是运行时由js代码去修改html 文档 ~

工具目的:

所以我们的目的就成了,浏览器文档一开始运行时:

就只引用了一个require.js文件

前端模块化之AMD与CMD原理(附源码)

执行了require.js之后,由js在dom上添加了一堆 <script>

前端模块化之AMD与CMD原理(附源码)

也就是说,红框里的<script>,都是require.js里的js代码手动在body元素尾部添加的!!!

还有,添加了<script src="3.js" > , 不能只是在html文档上添加了这行字符串,而是要 加载 并且 运行 3.js !!!

!!!重要方法:

1. 插入sciprt节点,并且加载+运行其js文件

// 建一个node节点, script标签
var node = document.createElement('script')
node.type = 'text/javascript'
node.src = '3.js'

// 将script节点插入dom中
document.body.appendChild(node)

复制代码

注意:

采用dom.appendChild方法插入script节点,会立即下载js文件,并且运行文件!

而采用dom.innerHTML = '<script src="3.js"></script>',则只是在dom中插入了一行字符串,就更不会管字符串里引入的js了,所以不能用这个方法插入script!!!

2. 各个js文件的插入时机

文件之间有依赖关系的,所以插入script节点是要有顺序的,比如:

1.js 依赖 2.js , 2.js 依赖 3.js ,所以浏览器加载顺序就应该是 先加载 3.js之后,再加载 2.js, 最后加载 1.js。那么就需要判断,3.js什么时候加载完呢?

<script src="3.js" onload="alert()"></script>
复制代码

关键就在于, onload 这个函数,其作用是,3.js 加载完并且执行完 之后,执行 alert

所以实现 3.js 加载完后 再加载 2.js ,则只需这样:

var node = document.createElement('script')
node.type = 'text/javascript'
node.src = '3.js'

// 给该节点添加onload事件
// 标签上onload,这里是load,见事件那里的知识点
// 3.js 加载完后
node.addEventListener('load', function(evt) {
    // 加载 2.js
    var node2 = document.createElement('script')
    node2.type = 'text/javascript'
    node2.src = '2.js
    document.body.appendChild(node2)
})

document.body.appendChild(node)

复制代码

所以,处理依赖的核心就是利用 onload 事件,不断的递归嵌套的加载依赖文件。事实上,最麻烦的也是这里处理依赖文件,尤其是一个文件可能被多个文件所依赖,的情况。

3. js文件中的函数的执行时机

这一点一定要理解,这也是require.js和sea.js的区别之一。

有些同学肯定会蒙了,加载?执行?执行啥?在加载成功的时候自己不就会瞬间执行了吗?

1.js 中

require([], functionA() {
    // 主要逻辑代码
})
复制代码

js文件加载后就瞬间执行的,是require()函数。而我们所经常说的执行,是指的 主要逻辑代码 这里所在的回调函数A!

所以 1.js 的回调函数A,是在所依赖的2、3都加载完后,才执行的。

所以,加载顺序和执行顺序是不一样的,比如还是这个 1、2、3.js 的依赖关系:

加载顺序:1.js,2.js,3.js

执行顺序:3.js,2.js,1.js(主模块在最后执行)

5. require.js 简单代码实现

用法例子

// 1.js 中(入口用require,其他用define)
require(['2.js'], function() {
    // 主要的执行代码
    // 2.js 3.js都加载完,才执行1.js的这回调函数!!!!!!!!!!!!!!!
})

// 2.js 中
define(['3.js'], function() {
    
})

// 3.js 中
define([], function() {
    
})

复制代码

require.js 简单源码原理

利用递归去加载层层的嵌套依赖,代码的难点就在于,怎样判断递归结束?即怎样判断所有的依赖都加载完了?

var modules = {},	// 存放所有文件模块的信息,每个js文件模块的信息
    loadings = [];	//	存放所有已经加载了的文件模块的id,一旦该id的所有依赖都
                                加载完后,该id将会在数组中移除

// 上面说了,每个文件模块都要有个id,这个函数是返回当前运行的js文件的文件名
// 比如,当前加载 3.js 后运行 3.js ,那么该函数返回的就是 '3.js'
function getCurrentJs() {
	return document.currentScript.src
}
// 创建节点
function createNode() {
	var node = document.createElement('script')
	node.type = 'text/javascript'
	node.async = true;
	return node
}
// 开始运行
function init() {
    // 加载 1.js
    loadJs('1.js')
}	
// 加载文件(插入dom中),如果传了回调函数,则在onload后执行回调函数
function loadJs(url, callback) {
    var node = createNode()
    node.src = url;
    node.setAttribute('data-id', url)
    node.addEventListener('load', function(evt) {
    	var e = evt.target
    	setTimeout(() => {  // 这里延迟一秒,只是让在浏览器上直观的看到每1秒加载出一个文件
    		callback && callback(e)
    	}, 1000)
    }, false)
    
    document.body.appendChild(node)
}	
	
// 此时,loadJs(1.js)后,并没有穿回调函数,所以1.js加载成功后只是自动运行1.js代码
// 而1.js代码中,是require(),则执行的是require函数, 在下面

window.require = function(deps, callback) {
    // deps 就是对应的 ['2.js']
    // callback 就是对应的 functionA
    // 在这里,是不会运行callback的,得等到依赖都加载完的
    // 所以得有个地方,把一个文件的所有信息都先存起来啊
    var id = getCurrentJs();// 当前运行的是1.js,所以id就是'1.js'
    if(!modules.id) {
    	modules[id] = { // 该模块对象信息
    		id: id,
    		deps: deps,
    		callback: callback,
    		exports: null,
    		status: 1, 
    		
    	}
    	loadings.unshift(id); // 加入这个id,之后会循环loadings数组,递归判断id所有依赖
    }
    
    loadDepsJs(id); // 加载这个文件的所有依赖,即去加载[2.js]
}

function loadDepsJs(id) {
    var module = modules[id]; // 获取到这个文件模块对象
    // deps是['2.js']
    module.deps.map(item => {   // item 其实是依赖的Id,即 '2.js'
        if(!modules[i]) {   // 如果这个文件没被加载过(注:加载过的肯定在modules中有)
        (1)    loadJs(item, function() {   // 加载 2.js,并且传了个回调,准备要递归了
                    // 2.js加载完后,执行了这个回调函数
                    loadings.unshift(item); // 此时里面有两个了, 1.js 和 2.js
                    // 递归。。。要去搞3.js了
                    loadDepsJs(item)// item传的2.js,递归再进来时,就去modules中取2.js的deps了
                    // 每次检查一下,是否都加载完了
                    checkDeps(); // 循环loadings,配合递归嵌套和modules信息,判断是否都加载完了
                })
        }
    })
}

// 上面(1)那里,加载了2.js后马上会运行2.js的,而2.js里面是
define(['js'], fn)
// 所以相当于执行了 define函数

window.define = function(deps,callback) {
    var id = getCurrentJs()
    if(!modules.id) {
        modules[id] = {
        	id: id,
        	deps: getDepsIds(deps),
        	callback: callback,
        	exports: null,
        	status: 1,
        	
        }
    }
}

// 注意,define运行的结果,只是在modules中添加了该模块的信息
// 因为其实在上面的loadDepsJs中已经事先做了loadings和递归deps的操作,
而且是一直不断的循环往复的进行探查,所以define里面就不需要再像require中写一次loadDeps了

// 循环loadings,查看loadings里面的id,其所依赖的所有层层嵌套的依赖模块是否都加载完了

function checkDeps() {
    for(var i = 0, id; i < loadings.length ; i++) {
	id = loadings[i]
	if(!modules[id]) continue
	
	var obj = modules[id], 
	deps = obj.deps
	
	// 为什么要执行checkCycle函数呢,checkDeps是循环loadings数组的模块id,而checkCycle是去判断该id模块所依赖的**层级**的模块是否加载完
	// 即checkDeps是**广度**的循环已经加载(但依赖没完全加载完的)的id
	// checkCycle是**深度**的探查所关联的依赖
	// 还是举例吧。。。假如还有个4.js,依赖5.js,那么
	// loadings中可能是 ['1.js', '4.js']
	// 所以checkDeps --> 1.js,  4.js
	// checkCycle深入内部 1.js --> 2.js --> 3.js
	// 4.js --> 5.js
	// 一旦比如说1.js的所有依赖2.js、3.js都加载完了,那么1.js 就会在loadings中移出
	
	
	var flag = myReq.checkCycle(deps)
	
	if(flag) {
            console.log(i, loadings[i] ,'全部依赖已经loaded');
		
            loadings.splice(i,1);
            // 不断的循环探查啊~~~~
            myReq.checkDeps()
	}
	
    }
}
	
复制代码

代码的难点就在于checkDeps以及对loadings进行递归那里,很难去讲清楚,需要自己去写去实践,这里也很难全都描述清楚。。。

结尾会给一个简单的能运行的例子

不要想着花一两个小时就搞定所有了,刚开始确实会看的烦,多回来几次,隔段时间再研究一下,每次都会加深一点

6. CMD

CMD 即Common Module Definition通用模块定义,sea.js是它的实现

sea.js是阿里的大神写得,和require.js很像,先看一下用法的区别

// 只有define,没有require
// 和AMD那个例子一样,还是1依赖2, 2依赖3
1.js中
define(function() {
    
    var a = require('2.js')
    console.log(33333)
    var b = require('4.js')
})

2.js 中
define(function() {
    var b = require('3.js')
})
3.js 中
define(function() {
    // xxx
})

复制代码

看着是比require.js要好一点。。。

AMD和CMD的区别

对依赖模块的 执行时机 不同,注意:不是加载的时机,模块加载的时机是一样的!!!

加载:还是先加载1.js,再加载2.js,最后加载3.js

执行顺序:

AMD:3.js,2.js,1.js,,,即如果模块以及该模块的依赖都加载完了,那么就执行。。。 3.js加载完后,发现自己也没有依赖啊,那么直接执行3.js的回调了,,,2.js加载完后探查到依赖的3.js也加载完了,那么2.js就执行自己的回调了。。。。 主模块一定在最后执行

CMD:1.js,2.js,3.js,,,即先执行主模块1.js,碰到require('2.js')就执行2.js,2.js中碰到require('3.js')就执行3.js

会不会又不理解,怎么能控制执行哪个文件模块呢?

还记得不,之前说过, 执行模块 ,是指的执行那个functionA回调函数,callback,,,那么这个callback函数其实在一开始已经赋到modules上了啊,所以无论CMD还是AMD, 执行模块 ,都是执行modules[id].callback()

前端模块化之AMD与CMD原理(附源码)

所以,sea.js里,你用的var a = require('2.js'),中的执行的require函数,源码中就是简单的执行了模块的callback

前端模块化之AMD与CMD原理(附源码)

7. sea.js源码

源码,大部分和require.js都很像,上面说的执行时机不同,也很简单,就是控制一下啥时执行callback呗。

之前又说了,加载模块差不多,那么sea.js是怎么通过require(3.js),require(2.js)去控制3.js和2.js的加载呢???上面说require函数已经就是执行callback了,那么require函数就不能承担起加载模块的功能了啊,再来看

AMD源码的define定义

window.define = function(deps,callback) {
    var id = getCurrentJs()
    if(!modules.id) {
        modules[id] = {
        	id: id,
        	deps: getDepsIds(deps),
        	callback: callback,
        	exports: null,
        	status: 1,
        	
        }
    }
}
复制代码

而CMD得define呢

用法

define(function() {
    var a = require('2.js')
})
复制代码

源码定义

window.define = function(callback) {
    var id = getCurrentJs()
    var depsInit = s.parseDependencies(callback.toString())
    var a = depsInit.map(item => basepath + item)
    // 就多了上面的2行代码
    // 1. 把传进来的函数给转换成字符串,'function (){var a = require("2.js")}'
    // 2. 利用一个正则函数,取出字符串中require中的2.js,最后拼成一个数组['2.js']返回来。
    // 3. 之后就和require.js差不多了啊。。。
    
    
    // 下面的都差不多
    if(!modules[id]) {
        modules[id] = {
            id: id,
            status: 1,
            callback: callback,
            deps: a,
            exports: null
        }
    }
    
    s.loadDepsJs(id)

    }
复制代码

所以sea.js,是写了一个正则的函数,去查询define中传入的fn的字符串,然后得到的依赖数组。。。 而require.js的依赖数组,是咱们自己写并且传入的:define([2.js])。。。

这个正则方法,大家不用去探究,练习时直接用就行了


以上所述就是小编给大家介绍的《前端模块化之AMD与CMD原理(附源码)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Pro HTML5 and CSS3 Design Patterns

Pro HTML5 and CSS3 Design Patterns

Michael Bowers / Apress / 2011-11-15 / GBP 35.50

Pro HTML5 and CSS3 Design Patterns is a reference book and a cookbook on how to style web pages using CSS3 and HTML5. It contains 350 ready--to--use patterns (CSS3 and HTML5 code snippets) that you ca......一起来看看 《Pro HTML5 and CSS3 Design Patterns》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具