AMD加载器实现笔记(一)
之前研究过AMD,也写过一篇关于AMD的文章《以代码爱好者角度来看AMD与CMD》。代码我是有看过的,基本的原理也都明白,但实际动手去实现却是没有的。因为今年计划的dojo教程《静静的dojo》中,有一章节来专门讲解AMD,不免要把对AMD的研究回炉一下。时隔多日,再回头探索AMD实现原理时,竟抓耳挠腮,苦苦思索不得要领。作为开发人员,深感惭愧。故有此文,记录我在实现一个AMD加载器时的思考总结。
requireJS是所有AMD加载器中,最广为人知的一个。目前的版本更凝聚了几位大牛数年心血,必然不是我这个小虾米一晚上的粗制滥造能够比拟的,所以目前为止这篇文章里的加载器尚不能称为AMD加载器。它并不支持AMD规范中对config的配置项,甚至不支持在define中明确地声明模块Id,而且它现在只支持chrome浏览器。它的API如下:
require([
'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb',
'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa.bbb.ccc',
'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc',
'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ddd',
'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff'], function(aaabbbccc){
console.log('simple loader');
console.log(arguments);
});
define(["http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(aaa){
$.log("已加载ccc模块")
return {
aaa: aaa,
ccc: "ccc555"
}
})
是的,目前并不支持模块解析功能,所以模块id只能是绝对路径。但对于一个简易的加载器已经足够,因为它还将会被迭代。
既然AMD是JavaScript模块化的解决方案,解决不支持模块化的JavaScript,那么任何一个解决方案都有必要在概念层面上去定义模块。在这里模块的定义是,使用define函数包装的js文件。既然是文件那首要解决加载的问题,异步无阻塞的的加载方式有多种解决方案,但最终被开发者广泛认可的是动态创建script标签的方式(不明白的同学去看一下这篇文章探真无阻塞加载javascript脚本技术,我们会发现很多意想不到的秘密)。
function loadJS(url) {
var script = document.createElement('script');
script.type = "text/javascript";
script.src = url + '.js';
script.onload = function() {
//干你的活
};
var head = document.getElementsByTagName('head')[0];
head.appendChild(script);
};
文件加载完毕后,会立即执行define函数。define函数包装后的模块在加载器内部的数据结构如下:
module:
- id: 模块的唯一标识
- deps:模块依赖项的标识数组
- factory:依赖项全部执行完毕后所执行的函数,所有模块的代码都写在这个函数里
- export:模块代码执行完毕后的输出对象
- state:模块的状态(AMD是要解决JavaScript模块依赖的问题,所以一个模块需要等待所有依赖项完成后才能执行模块的factory函数。我们需要state属性标识模块的状态,注册为1,执行完毕为2.)
我们先从define函数开始。
global.define = function(deps, callback) {
var id = getCurrentScript();
if (modules[id]) {
console.error('multiple define module: ' + id);
}
require(deps, callback, id);
};
function getCurrentScript(base) {
// 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
var stack;
try {
a.b.c(); //强制报错,以便捕获e.stack
} catch (e) { //safari的错误对象只有line,sourceId,sourceURL
stack = e.stack;
if (!stack && window.opera) {
//opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
}
}
if (stack) {
/**e.stack最后一行在所有支持的浏览器大致如下:
*chrome23:
* at http://113.93.50.63/data.js:4:1
*firefox17:
*@http://113.93.50.63/query.js:4
*opera12:http://www.oldapps.com/opera.php?system=Windows_XP
*@http://113.93.50.63/data.js:4
*IE10:
* at Global code (http://113.93.50.63/data.js:4:1)
* //firefox4+ 可以用document.currentScript
*/
stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符
return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置
}
var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找
for (var i = nodes.length, node; node = nodes[--i]; ) {
if ((base || node.className === moduleClass) && node.readyState === "interactive") {
return node.className = node.src;
}
}
};
getCurrentScript
我们的define仅支持匿名模块,所以第一件事便是需要一个模块id。根据这个id我们需要能够找出对应的Js文件。这里我们利用了Chrome的ReferenceError实例的stack属性。强制浏览器报错,获取error的stack属性,通过正则表达式匹配出文件的绝对路径。 依赖的模块的加载只需加载一次即可,禁止多次加载,所以遇到重复加载情况需要报错。注册模块与加载依赖项的工作交给了require函数来处理。
require函数是这里的大头,接下来我们便去揭开它的神秘面纱。
//module: id, state, factory, result, deps;
global.require = function(deps, callback, parent){
var id = parent || "Bodhi" + Date.now();
var cn = 0, dn = deps.length;
var args = []; var module = {
id: id,
deps: deps,
factory: callback,
state: 1,
result: null
};
modules[id] = module; deps.forEach(function(dep) {
if (modules[dep] && modules[dep].state === 2) {
cn++
args.push(modules[dep].result);
} else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
loadJS(dep);
loadedJs.push(dep);
}
});
if (cn === dn) {
callFactory(module);
} else {
//loadJS(id);// require只是用来加载其他模块的
loadings.push(id);
checkDeps();
}
};
因为define将责任推给了require,所以require的首要任务便是注册模块。JavaScript对于hash结构有着原生的支持,原生的对象{}做模块仓库最适合不过了。
接下来就是处理依赖项,如果模块的依赖项并未被加载,那就去加载它;另外记录下已加载的依赖模块数量。
如果依赖模块被执行完毕,那就去执行模块的factory函数;如果依赖项没有执行完毕,那就把模块id放入加载队列中,并执行依赖检查。
加载模块的工作交给了loadJs函数:
function loadJS(url) {
var script = document.createElement('script');
script.type = "text/javascript";
script.src = url + '.js';
script.onload = function() {
var module = modules[url];
if (module && isReady(module) && loadings.indexOf(url) > -1) {
callFactory(module);
}
checkDeps();
};
var head = document.getElementsByTagName('head')[0];
head.appendChild(script);
};
无论模块的依赖关系是多么复杂,当所有的依赖关系被确定后,必然有一个最后被等待的模块。这就好比武侠小说中,每个杀阵都有阵眼,只要破去阵眼就能破阵。我们称这最后被等待的模块为阵眼模块。当阵眼模块被执行完毕后,整个依赖网便被盘活,一层层的回归似的,执行factory函数。

而如何判断一个模块是阵眼模块呢?我们以deps为0作为依据。放在isRedy函数中。
function isReady(m) {
var deps = m.deps;
var allReady = deps.every(function(dep) {
return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
})
if (deps.length === 0 || allReady) {
return true;
}
};
而盘活的契机放在script的onload函数中。一个script元素的生命周期为:
创建元素-》加载脚本文件-》解析脚本文件(执行js代码)-》onload事件-》销毁
所以如果onload中模块是阵眼模块,或者依赖模块已被全部加载完毕,则执行factory函数。然后循环检查依赖,一层一层的盘活其他依赖网。
script.onload = function() {
var module = modules[url];
if (module && isReady(module) && loadings.indexOf(url) > -1) {
callFactory(module);
}
checkDeps();
};
整个加载器代码如下:
(function(global){
global.$ = {
log: function(m) {
console.log(m);
}
};
global = global || window;
modules = {};
loadings = [];
loadedJs = [];
//module: id, state, factory, result, deps;
global.require = function(deps, callback, parent){
var id = parent || "Bodhi" + Date.now();
var cn = 0, dn = deps.length;
var args = [];
var module = {
id: id,
deps: deps,
factory: callback,
state: 1,
result: null
};
modules[id] = module;
deps.forEach(function(dep) {
if (modules[dep] && modules[dep].state === 2) {
cn++
args.push(modules[dep].result);
} else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
loadJS(dep);
loadedJs.push(dep);
}
});
if (cn === dn) {
callFactory(module);
} else {
//loadJS(id);// require只是用来加载其他模块的
loadings.push(id);
checkDeps();
}
};
global.define = function(deps, callback) {
var id = getCurrentScript();
if (modules[id]) {
console.error('multiple define module: ' + id);
}
require(deps, callback, id);
};
function loadJS(url) {
var script = document.createElement('script');
script.type = "text/javascript";
script.src = url + '.js';
script.onload = function() {
var module = modules[url];
if (module && isReady(module) && loadings.indexOf(url) > -1) {
callFactory(module);
}
checkDeps();
};
var head = document.getElementsByTagName('head')[0];
head.appendChild(script);
};
function checkDeps() {
for (var p in modules) {
var module = modules[p];
if (isReady(module) && loadings.indexOf(module.id) > -1) {
callFactory(module);
checkDeps(); // 如果成功,在执行一次,防止有些模块就差这次模块没有成功
}
}
};
function isReady(m) {
var deps = m.deps;
var allReady = deps.every(function(dep) {
return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
})
if (deps.length === 0 || allReady) {
return true;
}
};
function callFactory(m) {
var args = [];
for (var i = 0, len = m.deps.length; i < len; i++) {
args.push(modules[m.deps[i]].result);
}
m.result = m.factory.apply(window, args);
m.state = 2;
var idx = loadings.indexOf(m.id);
if (idx > -1) {
loadings.splice(idx, 1);
}
};
function getCurrentScript(base) {
// 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
var stack;
try {
a.b.c(); //强制报错,以便捕获e.stack
} catch (e) { //safari的错误对象只有line,sourceId,sourceURL
stack = e.stack;
if (!stack && window.opera) {
//opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
}
}
if (stack) {
/**e.stack最后一行在所有支持的浏览器大致如下:
*chrome23:
* at http://113.93.50.63/data.js:4:1
*firefox17:
*@http://113.93.50.63/query.js:4
*opera12:http://www.oldapps.com/opera.php?system=Windows_XP
*@http://113.93.50.63/data.js:4
*IE10:
* at Global code (http://113.93.50.63/data.js:4:1)
* //firefox4+ 可以用document.currentScript
*/
stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符
return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置
}
var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找
for (var i = nodes.length, node; node = nodes[--i]; ) {
if ((base || node.className === moduleClass) && node.readyState === "interactive") {
return node.className = node.src;
}
}
};
})(window)
测试代码:
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
<title>Web AppBuilder for ArcGIS</title>
<link rel="shortcut icon" href="builder/images/shortcut.png">
</head>
<body class="claro">
<script src="./loader.js"></script>
<script>
require([
'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb',
'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa.bbb.ccc',
'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc',
'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ddd',
'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff'], function(aaabbbccc){
console.log('simple loader');
console.log(arguments);
});
</script>
</body>
</html>
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc"],function(a, c){
console.log("已加载bbb模块", 7)
return {
aaa: a,
ccc: c.ccc,
bbb: "bbb"
}
})
bbb
define([], function(){
console.log("已加载aaa.bbb.ccc模块", 7)
return "aaa.bbb.ccc";
});
aaa.bbb.ccc
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(aaa){
$.log("已加载ccc模块")
return {
aaa: aaa,
ccc: "ccc555"
}
})
ccc
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc",
"http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff"],function(a,b,c,f){
$.log("已加载ddd模块", 7);
return {
bbb: b,
ddd: "ddd",
length: arguments.length
}
})
ddd
define(['http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/more/ggg'], function(g){
$.log("已加载fff模块")
return {
ggg: g,
fff: "fff"
}
})
fff
define([], function(){
console.log("已加载aaa模块", 7)
return "aaa"
});
aaa
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/more/ggg"],function(ret){
$.log("已加载eee模块",7)
return {
eee: "eee",
aaa: ret.aaa,
ggg: ret.ggg
}
})
eee
define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(a){
$.log("已加载ggg模块",7)
return {
aaa: a,
ggg:"ggg"
}
})
ggg
执行结果如下:
已加载aaa模块 7
loader.js:4 已加载ggg模块
loader.js:4 已加载fff模块
aaa.bbb.ccc.js:2 已加载aaa.bbb.ccc模块 7
loader.js:4 已加载ccc模块
bbb.js:3 已加载bbb模块 7
loader.js:4 已加载ddd模块
index.html:19 simple loader
index.html:20 Arguments[5]
下一篇文章将会为我们的加载器加上模块路径解析功能,到时候我们便不用书写如此丑陋的模块id了。
如果您觉得这篇文章对您有帮助,请不吝点击右下方推荐~
AMD加载器实现笔记(一)的更多相关文章
- AMD加载器实现笔记(二)
AMD加载器实现笔记(一)中,我们实现了一个简易的模块加载器.但到目前为止这个加载器还并不能称为AMD加载器,原因很简单,我们还不支持AMD规范中的config配置.这篇文章中我们来添加对config ...
- AMD加载器实现笔记(五)
前几篇文章对AMD规范中的config属性几乎全部支持了,这一节主要是进一步完善.到目前为止我们的加载器还无法处理环形依赖的问题,这一节就是解决环形依赖. 所谓环形依赖,指的是模块A的所有依赖项的依赖 ...
- AMD加载器实现笔记(四)
继续这一系列的内容,到目前为止除了AMD规范中config的map.config参数外,我们已经全部支持其他属性了.这一篇文章中,我们来为增加对map的支持.同样问题,想要增加map的支持首先要知道m ...
- AMD加载器实现笔记(三)
上一篇文章中我们为config添加了baseUrl和packages的支持,那么这篇文章中将会看到对shim与paths的支持. 要添加shim与paths,第一要务当然是了解他们的语义与用法.先来看 ...
- AngularJs2与AMD加载器(dojo requirejs)集成
现在是西太平洋时间凌晨,这个问题我鼓捣了一天,都没时间学英语了,英语太差,相信第二天我也看不懂了,直接看结果就行. 核心原理就是require在AngularJs2编译过程中是关键字,而在浏览器里面运 ...
- Promise实现简易AMD加载器
在最新的Chrome和FF中已经 实现了Promise.有了Promise我们用数行代码即可实现一个简易AMD模式的加载器 var registry = { promises: { }, resolv ...
- JavaScript AMD 模块加载器原理与实现
关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下. 1. 模块加载器 模块加载器目前比较流行的有 Requirejs 和 Seajs.前者遵循 AMD规范,后者 ...
- KnockoutJS 3.X API 第六章 组件(5) 高级应用组件加载器
无论何时使用组件绑定或自定义元素注入组件,Knockout都将使用一个或多个组件装载器获取该组件的模板和视图模型. 组件加载器的任务是异步提供任何给定组件名称的模板/视图模型对. 本节目录 默认组件加 ...
- 构建服务端的AMD/CMD模块加载器
本文原文地址:http://trock.lofter.com/post/117023_1208040 . 引言: 在前端开发领域,相信大家对AMD/CMD规范一定不会陌生,尤其对requireJS. ...
随机推荐
- ASP.NET MVC分页组件MvcPager 2.0版发布暨网站全新改版
MvcPager分页控件是在ASP.NET MVC Web应用程序中实现分页功能的一系列扩展方法,该分页控件的最初的实现方法借鉴了网上流行的部分源代码, 尤其是ScottGu的PagedList< ...
- Mysql连接到Visual studio注意
测试环境:mysql(mysql-connector-net-6.9.7.msi)+visual studio2012 1.mysql安装ok的情况下!在控制面板管理工具-数据源(ODBC)--用户D ...
- 类Arrays
package p2; import java.util.Arrays; import java.util.List; public class ArraysDemo { public static ...
- Android视图状态及重绘流程分析,带你一步步深入了解View(三)
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/17045157 在前面一篇文章中,我带着大家一起从源码的层面上分析了视图的绘制流程, ...
- esri联邦用户大会 总结
1 概述 1.1 文档概述 ESRI联邦用户大会已于2月25日到2月27日在美国华盛顿举行,现就其会议中设计到的内容总结如下: 1.2 景观分析 这是第一个demo,演示的是"景观分析&qu ...
- 移动App崩溃的测试用例设计
我们的日常生活中对移动设备越来越多的使用意味着移动App测试这个主题已成为需要考虑的一个无法避免的问题.根据最近的调查研究,用户难以容忍有bug的移动App. 移动App Bug的影响是用户体验差.A ...
- MySQL入门手册
本文内容摘自MySQL5.6官方文档,主要选取了在实践过程中所用到的部分文字解释,力求只摘录重点,快速学会使用MySQL,本文所贴代码地方就是我亲自练习过的代码,凡本文没有练习过的代码都没有贴在此处, ...
- Mini projects #6 ---- Blackjack
课程全名:An Introduction to Interactive Programming in Python,来自 Rice University 授课教授:Joe Warren, Scott ...
- FMDB浅析
一.FMDB介绍 FMDB是一种第三方的开源库,FMDB就是对SQLite的API进行了封装,加上了面向对象的思想,让我们不必使用繁琐的C语言API函数,比起直接操作SQLite更加方便. FMDB优 ...
- 测试架构图 High Level 产品技术(无事来更新,证明这个博客还是Live的)
一个完整的产品测试所需要掌握的产品技术架构. 1.最底层硬件平台(服务器与存储) 对于一个大型商业解决方案来说,性能与可靠性是非常重要的要求,那么服务器与存储就是专门来满足需求的. 服务器: 服务器端 ...