之前研究过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了。

  

  如果您觉得这篇文章对您有帮助,请不吝点击右下方推荐~

最新文章

  1. 求1+2+……+n(位运算)
  2. 修改内联CSS(点击按钮连续改变文字大小、位置,.animate()方法)
  3. sqlite加密
  4. Linux的五个查找命令(find、locate、whereis、which、type)
  5. DTD 知识归纳总结
  6. 有关android UI 线程
  7. win7安装memcached
  8. 使用最新的log4cplus(1.1.1)隔离不同的 log 文件输出
  9. linux下编译php追加enable的方法
  10. 【Linux配置】vim配置文件内容
  11. iconfont 使用
  12. windows主用python3 个别程序使用python2的方法
  13. Select2插件的隐藏、设置宽度
  14. Oracle联合多个子查询(inner join)
  15. zzulioj 1734 堆
  16. Linux 下 c 语言 聊天软件
  17. [LeetCode] 88. Merge Sorted Array_Easy tag: Two Pointers
  18. Android内核和Linux内核的区别
  19. OA项目(MVC项目)
  20. 1. [文件]- 文件类型,文件open模式

热门文章

  1. Spring Boot + Bootstrap 出现&quot;Failed to decode downloaded font&quot;和&quot;OTS parsing error: Failed to convert WOFF 2.0 font to SFNT&quot;
  2. webrtc进阶-信令篇-之三:信令、stun、turn、ice
  3. Codeigniter 在Active Record中限制批量更新数目
  4. centOS升级python3.5
  5. 查找原始MySQL死锁ID
  6. Debian使用相关
  7. 简单的css js控制table隔行变色
  8. 原生js-焦点图轮播
  9. 【转】Backbone使用总结
  10. Python学习之路--面向对象