前端模块化之AMD与CMD原理(附源码)
栏目: JavaScript · 发布时间: 5年前
内容简介:可能现在初入前端的同学们,都直接就上手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. 需求的产生
如上图,一开始前端的内容不多,主要是页面和简单的交互,所以前端的开发环境就是纯几个静态文件(html、css、js)。而html文件这样子引入js文件,有很多缺点:
- 必须按顺序引入,如果 1.js中要用到jquery,那就将jquery.js放到1.js上方。
- 同步加载各个js,只有1.js加载并执行完,才去加载2.js。
- 各个js文件可能会有多个window全局变量的创建,污染。
- ......还有很多缺点
总之,上面的结构,在前端内容越来越多,尤其ajax的趋势、前后端分离、越来越注重前端体验,js文件越来越多且互相引用更复杂的情况下,真心乱套了,所以需要有一个新的模块化工具。
而模块化希望的样子:
html文件中
1.js文件中
2.js中
我们当然是希望这样,improt、export就行了,但遗憾的是,这是es6配合node的用法,而以前的AMD、CMD可不是这么实现的。
3. AMD
即Asynchronous Module Definition,中文名是 异步模块定义 的意思。它是一个模块化开发的规范,是一种思路,是一个概念。我们不用过于纠结它到底是个啥,只需要知道它是一个规范概念,而大名鼎鼎的require.js,是它的一个具体的实现,以前很多都用这个 工具 开发,接下来我们就研究一下require.js。
require.js的用法
这里只介绍大概简单的用法,没用过的同学最好去看教程。
login.html中
引入了require.js文件,然后指定了入口文件login.js
~~
入口文件login.js中
loginModule.js中
loginCtrl.js中
注意:
-
实际上就是定义了两个全局变量函数,一个require(),一个define()
-
其实这两个函数功能和原理差不多,你可以认为他俩除了名字不一样,其他都差不多。
-
define函数的第一个参数是个数组,写的是所依赖的模块;第二个参数是回调函数,回调函数里的参数对应的是依赖数组里的模块返回值,如:
es6 :
import A from 'a.js'
import B from 'b.js'
require.js:
define(['a.js', 'b.js'], function(A, B) {
})
-
从几个图中可以看到依赖的顺序,所以加载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了,让编码更爽。。。但是对于浏览器那边,没啥大的变化。
以前:
编码:
浏览器:
require.js后
编码:
浏览器:
所以结论:require.js只是采取了某种“方法”,让你在写代码时只写一个<script>,运行html文档时却是多个<script>
分析实现原理:
可能1:
编码时的html文件和运行时的html文件是两个文件,即通过某些工具复制并修改了html。可惜修改文件需要服务端程序去做,而require.js只是个js文件,所以不是这个原理。这在node下webpack可以轻松的实现。
可能2:
既然可能1是不对的,那么说明了,浏览器运行的html文件和编码时的html文件是一模一样的。所以只剩下第二条路了,就是运行时由js代码去修改html 文档 ~
工具目的:
所以我们的目的就成了,浏览器文档一开始运行时:
就只引用了一个require.js文件
执行了require.js之后,由js在dom上添加了一堆 <script>
也就是说,红框里的<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()
所以,sea.js里,你用的var a = require('2.js'),中的执行的require函数,源码中就是简单的执行了模块的callback
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原理(附源码)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。