目的:

了解AMD规范与CMD规范,写一个模块加载器雏形。

基本概念:

AMD是异步模块定义规范,而CMD是通用模块定义规范。其他的还有CommonJS Modules规范。

对于具体的规范,可以参考:

https://github.com/amdjs/amdjs-api/wiki/AMD AMD规范

https://github.com/seajs/seajs/issues/242 CMD规范

http://www.zhihu.com/question/20351507/answer/14859415 玉伯对于AMD和CMD规范的回答

无论是哪一种规范,都是为了解决前端模块依赖与加载的问题。

与后台语言不同的地方是,js并无法做到阻塞的模块/文件加载,这是我们需要这些模块加载、依赖管理工具的重要原因之一。

需求分析:

根据规范,我们需要实现require和define两个接口,define接口用于定义模块(并加载依赖),require接口用来加载模块并回调。

我们将实现简化的接口:

define
    define(deps, definition):依赖数组 与 模块定义函数
    define(definition):模块定义函数(模块无依赖)

require
    require(deps, callback):依赖数组 与 完成回调
    require(moduleId):传入模块id,获得模块实例(模块需提前通过define或require完成加载)

接下来我们进一步分析下任务:

代码(模块)状态:
未加载 -> 加载中 -> 加载完毕,等待依赖解决 -> 依赖完成,模块加载完成 ( -> 初次使用,执行模块定义生成模块实例)
非模块代码无依赖,加载后直接跳到完成状态,并且无模块定义无法生成模块实例

模块的加载时间线:
开始加载,添加加载完成的回调 -> 加载完成,执行define函数,注册模块依赖与模块定义 -> 执行代码加载完成回调(从这里拿到代码的url,或者说是模块ID),解析模块

我们需要在几个情况下分析依赖是否满足:
代码加载完成时以及模块依赖满足,状态为完成时

依赖满足的条件:
所有依赖的模块状态为完成

对于js的加载,我们创建script标签,在其上保存模块的id,并添加加载完成的时间侦听:

var scriptNode = document.createElement('script');
scriptNode.async = true;
scriptNode.moduleId = mId;
scriptNode.addEventListener('load', onScriptLoaded);
scriptNode.src = mId + '.js';
document.body.appendChild(scriptNode);

最终代码:

load.js:

(function (global) {
	var modules = {}, // 模块列表
		_modulesToLoad = [], // 从依赖中获得的要加载的js列表
		MODULE_STATUS = { // 定义模块状态
			LOADING: 1,
			PENDING: 2,
			DONE: 3
		},
		_modulesToHandler = [], // define函数运行时存入这个数组(在这里每次只有一个被存入),在onScriptLoaded中处理
		_requireCallbacks = []; // 待满足的依赖,每一项格式为[deps数组,回调函数]

	// 处理依赖,得到要加载的模块数组,并建立LOADING的模块状态
	function addDepsToModules (deps) {
		if (!deps) { return; }
		deps.forEach(function (mId) {
			if (!modules[mId]) {
				modules[mId] = {
					id: mId,
					status: MODULE_STATUS.LOADING,
					deps: null,
					defi: null,
					instance: null
				}
				_modulesToLoad.push(mId);
			}
		});
	}
	// 加载代码,在script标签节点上保存模块id
	function loadScripts () {
		var mId;
		while(mId = _modulesToLoad.pop()) {
			var scriptNode = document.createElement('script');
			scriptNode.async = true;
			scriptNode.moduleId = mId;
			scriptNode.addEventListener('load', onScriptLoaded);
			scriptNode.src = mId + '.js';
			document.body.appendChild(scriptNode);
		}
	}
	function onScriptLoaded (evt) {
		if (evt.type === 'load') {
			var scriptNode = evt.currentTarget,
				mId = scriptNode.moduleId;
			if (modules[mId]) { // 不处理作为data-main的入口代码
				if (_modulesToHandler.length === 0) { // 加载的并非模块代码(没有调用define),立即修改状态为DONE
					modules[mId].status = MODULE_STATUS.DONE;
				} else {
					var pair = _modulesToHandler.pop(),
						deps = pair[0],
						defi = pair[1];
					modules[mId].status = MODULE_STATUS.PENDING; // 修改状态问PENDING
					modules[mId].defi = defi; // 保存模块定义
					_requireCallbacks.push([deps, function () { // 建立待满足的依赖
						modules[mId].status = MODULE_STATUS.DONE; // 依赖满足后修改状态为DONE
						setTimeout(resolveRequireCallbacks, 0); // 并重新寻找已满足的依赖
					}]);
					addDepsToModules(deps); // 处理这个模块的依赖
					loadScripts(); // 加载依赖的js
				}
				resolveRequireCallbacks(); // 寻找已满足的依赖
			}
		}
	}
	// 寻找并处理已满足的依赖
	function resolveRequireCallbacks () {
		_requireCallbacks = _requireCallbacks.filter(function (pair) {
			var deps = pair[0],
				callback = pair[1],
				allLoaded = true;
			// 判断依赖是否都已满足
			deps && deps.some(function (mId) {
				if (modules[mId].status !== MODULE_STATUS.DONE) {
					allLoaded = false;
					return true;
				}
			});
			if (allLoaded) { // 如果满足执行回调,并剔除出待满足的依赖列表
				callback();
				return false;
			} else {
				return true;
			}
		});
	}
	// 定义require函数
	global.require = function (deps, callback) {
		if (typeof deps === 'string') { // 传入moduleId时返回模块实例
			if (modules[deps] && modules[deps].status === MODULE_STATUS.DONE) {
				if (!modules[deps].instance) { // 模块实例在第一次require(mId)时通过模块定义创建
					modules[deps].instance = modules[deps].defi();
				}
				return modules[deps].instance;
			} else {
				throw 'Module ['+deps+'] not loaded yet.';
			}
		}
		_requireCallbacks.push([deps, callback]); // 添加回调函数到待解决的依赖
		addDepsToModules(deps); // 寻找要加载的js
		loadScripts(); // 加载js
		resolveRequireCallbacks(); // 处理待满足的依赖
	}
	// 定义define函数
	global.define = function (deps, defi) {
		if (!defi) {
			defi = deps;
			deps = null;
		}
		// 将依赖、模块定义存入待处理数组,由onScriptLoaded处理
		_modulesToHandler.push([deps, defi]);
	}
	// 获得并加载main入口js
	var scripts = document.getElementsByTagName('script'),
		selfScript = scripts[scripts.length - 1], // 当前代码,也就是指向load.js的这个
		mainSrc = selfScript.getAttribute('data-main');
	global.onload = function () { // 等待body完成创建,应当在window.onload前就执行,这里就直接用window.onload了
		_modulesToLoad.push(mainSrc);
		loadScripts();
	}
})(window);

html:

<script data-main="main" src="load.js"></script>

模块文件:

// main.js
require(['d', 'd0'], function() {
    console.log('ready', require('d').mId, require('d0').mId);
});
// d.js
define(['d1', 'd2'], function () {
	return {mId: 'd' + require('d1').token};
});
// d0.js
define(['d1', 'd2'], function () {
	return {mId: 'd0' + require('d1').token};
});
// d1.js
define(function () {
	return {token: '|xyz123'};
});
// d2.js
console.log('d2');

可以做的改进:

1. 依赖锁死的情况,比如模块依赖自身,模块循环依赖的情况,改如何处理。

2. 单个文件中多个define的调用,调用形式为define(模块id,模块依赖,模块定义),这样做是为了使得多个模块代码可以合并到单文件中。

3. 更智能的模块依赖分析。

对于这个第三点,我们来做个小尝试:

function func (jquery, underscore) {
	var async = require('async');
}
func.toString();

我们可以看到利用function的toString方法,通过“分析代码”,我们可以从模块定义函数“智能”的分析出依赖,无论是angular的依赖注入还是sea.js的CMD编写格式,就都可以做到了。

扩展:

除了require.js之外,browserify和webpack都是采用了CommonJS规范,这样的好处是与nodejs以及ES6的模块规范保持一致。

browserify:http://browserify.org/

webpack:http://webpack.github.io/

与require不同,这两种工具都要求代码提前通过编译转换,特别是browserify,生成的文件添加的代码相对较少,加之本身丰富的功能,很适合单页面应用和多页面的项目,但这两者的定位并非解决动态加载和相应的模块依赖。

对于相对小型或简单的项目,webpack是很好的选择,一是配置简单,二是代码配重小。但对于大型的单页面应用,当我们需要动态加载大量模块时,require可以压缩“静态依赖”/框架代码,并动态加载模块代码,因此仍然是非常好的选择之一。

js与AMD模块加载的更多相关文章

  1. 一个简单的AMD模块加载器

    一个简单的AMD模块加载器 参考 https://github.com/JsAaron/NodeJs-Demo/tree/master/require PS Aaron大大的比我的完整 PS 这不是一 ...

  2. javascript Date 日期格式化 formatDate, require.js 模块 支持全局js引入 / amd方式加载

    * 引入AMD加载方式: require.js CDN https://cdn.bootcss.com/require.js/2.3.5/require.js *  创建模块文件./js/util/d ...

  3. JavaScript AMD 模块加载器原理与实现

    关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下. 1. 模块加载器 模块加载器目前比较流行的有 Requirejs 和 Seajs.前者遵循 AMD规范,后者 ...

  4. 对于requirejs AMD模块加载的理解

    个人人为使用模块化加载的优点有三: 1,以我的项目为例:90%为图表展示,使用的是echarts,此文件较大,requirejs可以在同一个版本号(urlArgs)之下缓存文件,那么我就可以在访问登陆 ...

  5. 【模块化编程】理解requireJS-实现一个简单的模块加载器

    在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...

  6. Dojo初探之1:AMD规范,编写符合AMD规范(异步模块加载机制)的模块化JS(其中dojo采用1.11.2版本)

    一.AMD规范探索 1.AMD规范(即异步模块加载机制) 我们在接触js的时候,一般都是通过各种function来定义一些方法,让它们帮我们做一些事情,一个js可以包含很多个js,而这些functio ...

  7. js模块加载之AMD和CMD

    当我写这篇文章的时候,sea.js已经逐渐退出历史的舞台,详细链接.不过任何新事物的出现都是对旧事物的取其精华,去其糟粕,所以了解一下以前模块的加载也是一件好事. js模块化的原因自不比多说,看看HU ...

  8. js模块定义——支持CMD&AMD&直接加载

    /* animate */ //直接加载 (function() { var animate = {} //balabala window.animate = animate; })(); //AMD ...

  9. 如何使用 require.js ,实现js文件的异步加载,避免网页失去响应,管理模块之间的依赖性,便于代码的编写和维护。

    一.为什么要用require.js? 最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了.后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载.下面的网页代 ...

随机推荐

  1. FZU Problem 2029 买票问题(树状数组)

    当我看到题目是5秒的时候,压根没有想树状数组,一直奔着模拟队列去想了,最后也没搞定,赛后看到大神的题解才恍然大悟,原来如此,题目中有明显的暗示,求前n项和,骤然感叹,自己太low... 果然还是要多做 ...

  2. dedecms mysql连接错误:#1040 - Too many connections

    mysql能登进去一下,点任何链接又跳出来,然后就登不上了 解决办法:检查mysql所在盘是否还有空间

  3. 最短路径问题 HDU 3790

    最短路径问题 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Subm ...

  4. lang

    我的docker容器里边,运行我的java进程时环境变量LANG为空导致乱码,重启java进程不为空显示正常:没有地方显式的设置过LANG,没找到/etc/(environment,profile,l ...

  5. Java基本数据类型和关键字

    变量名第一个字母小写,后面大写. 自动类型转换: 容量小的类型自动转换成容量大的数据类型 byte,short,int->float->long->double byte,short ...

  6. CodeForces 618B Guess the Permutation

    只要找出当前没用过的数字中,大于或等于当前这一列的最大值就可以 #include<cstdio> #include<cstring> #include<cmath> ...

  7. Android中SQLite的使用

    SQLite是Android中内置的数据库,SQLite是轻量级数据库,支持标准的SQL语法,并且支持ACID事物. 在Android中提供了SQLIteOPenHelper类,帮助我们使用SQLit ...

  8. IT技术网站汇总

    首先是比较著名的博客型的网站!一般来说在国外比较著名的博客基本上都是比较有影响力发起的或者建立的经常发布一些比较有思考力深入分析的文章! 博客媒体网站 1.www.ArsTechnica.com 2. ...

  9. LPC1768外部中断与GPIO中断

    LPC1768的外部中断严格来说只有四个,分别是EINT0,EINT1,EINT2,EINT3,技术手册上有如下说明 控制这四个外部中断靠以下寄存器 这三个寄存器的0 1 2 3位分别代表中断的0 1 ...

  10. ASIHTTPRequest异步请求 分类: ios技术 2015-03-01 09:33 48人阅读 评论(0) 收藏

    我们运行程序,如果网速很慢,查询的时候会一直黑屏,直到请求结束画面才出现,这样用户体验很不好.因此同步请求一般只是在某个子线 程中使用,而不在主线程中使用.异步请求的用户体验要比同步请求好,因此一般情 ...