之前研究过AMD,也写过一篇关于AMD的文章《以代码爱好者角度来看AMD与CMD》。代码我是有看过的,基本的原理也都明白,但实际动手去实现却是没有的。因为今年计划的dojo教程《静静的dojo》中,有一章节来专门讲解AMD,不免要把对AMD的研究回炉一下。时隔多日,再回头探索AMD实现原理时,竟抓耳挠腮,苦苦思索不得要领。作为开发人员,深感惭愧。故有此文,记录我在实现一个AMD加载器时的思考总结。

  requireJS是所有AMD加载器中,最广为人知的一个。目前的版本更凝聚了几位大牛数年心血,必然不是我这个小虾米一晚上的粗制滥造能够比拟的,所以目前为止这篇文章里的加载器尚不能称为AMD加载器。它并不支持AMD规范中对config的配置项,甚至不支持在define中明确地声明模块Id,而且它现在只支持chrome浏览器。它的API如下:

  1. require([
  2. 'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb',
  3. 'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa.bbb.ccc',
  4. 'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc',
  5. 'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ddd',
  6. 'http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff'], function(aaabbbccc){
  7. console.log('simple loader');
  8. console.log(arguments);
  9. });
  1. define(["http://lzz-pc.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(aaa){
  2. $.log("已加载ccc模块")
  3. return {
  4. aaa: aaa,
  5. ccc: "ccc555"
  6. }
  7. })

  是的,目前并不支持模块解析功能,所以模块id只能是绝对路径。但对于一个简易的加载器已经足够,因为它还将会被迭代。

  

  既然AMD是JavaScript模块化的解决方案,解决不支持模块化的JavaScript,那么任何一个解决方案都有必要在概念层面上去定义模块。在这里模块的定义是,使用define函数包装的js文件。既然是文件那首要解决加载的问题,异步无阻塞的的加载方式有多种解决方案,但最终被开发者广泛认可的是动态创建script标签的方式(不明白的同学去看一下这篇文章探真无阻塞加载javascript脚本技术,我们会发现很多意想不到的秘密)。

  1. function loadJS(url) {
  2. var script = document.createElement('script');
  3. script.type = "text/javascript";
  4. script.src = url + '.js';
  5. script.onload = function() {
  6. //干你的活
  7. };
  8. var head = document.getElementsByTagName('head')[0];
  9. head.appendChild(script);
  10. };

  文件加载完毕后,会立即执行define函数。define函数包装后的模块在加载器内部的数据结构如下:

  module:

  • id: 模块的唯一标识
  • deps:模块依赖项的标识数组
  • factory:依赖项全部执行完毕后所执行的函数,所有模块的代码都写在这个函数里
  • export:模块代码执行完毕后的输出对象
  • state:模块的状态(AMD是要解决JavaScript模块依赖的问题,所以一个模块需要等待所有依赖项完成后才能执行模块的factory函数。我们需要state属性标识模块的状态,注册为1,执行完毕为2.)

  

  我们先从define函数开始。

  1. global.define = function(deps, callback) {
  2. var id = getCurrentScript();
  3. if (modules[id]) {
  4. console.error('multiple define module: ' + id);
  5. }
  6.  
  7. require(deps, callback, id);
  8. };
  1. function getCurrentScript(base) {
  2. // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
  3. var stack;
  4. try {
  5. a.b.c(); //强制报错,以便捕获e.stack
  6. } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
  7. stack = e.stack;
  8. if (!stack && window.opera) {
  9. //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
  10. stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
  11. }
  12. }
  13. if (stack) {
  14. /**e.stack最后一行在所有支持的浏览器大致如下:
  15. *chrome23:
  16. * at http://113.93.50.63/data.js:4:1
  17. *firefox17:
  18. *@http://113.93.50.63/query.js:4
  19. *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
  20. *@http://113.93.50.63/data.js:4
  21. *IE10:
  22. * at Global code (http://113.93.50.63/data.js:4:1)
  23. * //firefox4+ 可以用document.currentScript
  24. */
  25. stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
  26. stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符
  27. return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置
  28. }
  29. var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找
  30. for (var i = nodes.length, node; node = nodes[--i]; ) {
  31. if ((base || node.className === moduleClass) && node.readyState === "interactive") {
  32. return node.className = node.src;
  33. }
  34. }
  35. };

getCurrentScript

  我们的define仅支持匿名模块,所以第一件事便是需要一个模块id。根据这个id我们需要能够找出对应的Js文件。这里我们利用了Chrome的ReferenceError实例的stack属性。强制浏览器报错,获取error的stack属性,通过正则表达式匹配出文件的绝对路径。 依赖的模块的加载只需加载一次即可,禁止多次加载,所以遇到重复加载情况需要报错。注册模块与加载依赖项的工作交给了require函数来处理。

  require函数是这里的大头,接下来我们便去揭开它的神秘面纱。

  1. //module: id, state, factory, result, deps;
  2. global.require = function(deps, callback, parent){
  3. var id = parent || "Bodhi" + Date.now();
  4. var cn = 0, dn = deps.length;
  5. var args = [];
  6.  
  7. var module = {
  8. id: id,
  9. deps: deps,
  10. factory: callback,
  11. state: 1,
  12. result: null
  13. };
  14. modules[id] = module;
  15.  
  16. deps.forEach(function(dep) {
  17. if (modules[dep] && modules[dep].state === 2) {
  18. cn++
  19. args.push(modules[dep].result);
  20. } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
  21. loadJS(dep);
  22. loadedJs.push(dep);
  23. }
  24. });
  25. if (cn === dn) {
  26. callFactory(module);
  27. } else {
  28. //loadJS(id);// require只是用来加载其他模块的
  29. loadings.push(id);
  30. checkDeps();
  31. }
  32. };

  因为define将责任推给了require,所以require的首要任务便是注册模块。JavaScript对于hash结构有着原生的支持,原生的对象{}做模块仓库最适合不过了。

  接下来就是处理依赖项,如果模块的依赖项并未被加载,那就去加载它;另外记录下已加载的依赖模块数量。

  如果依赖模块被执行完毕,那就去执行模块的factory函数;如果依赖项没有执行完毕,那就把模块id放入加载队列中,并执行依赖检查。

  加载模块的工作交给了loadJs函数:

  1. function loadJS(url) {
  2. var script = document.createElement('script');
  3. script.type = "text/javascript";
  4. script.src = url + '.js';
  5. script.onload = function() {
  6. var module = modules[url];
  7. if (module && isReady(module) && loadings.indexOf(url) > -1) {
  8. callFactory(module);
  9. }
  10. checkDeps();
  11. };
  12. var head = document.getElementsByTagName('head')[0];
  13. head.appendChild(script);
  14. };

  无论模块的依赖关系是多么复杂,当所有的依赖关系被确定后,必然有一个最后被等待的模块。这就好比武侠小说中,每个杀阵都有阵眼,只要破去阵眼就能破阵。我们称这最后被等待的模块为阵眼模块。当阵眼模块被执行完毕后,整个依赖网便被盘活,一层层的回归似的,执行factory函数。

  而如何判断一个模块是阵眼模块呢?我们以deps为0作为依据。放在isRedy函数中。

  1. function isReady(m) {
  2. var deps = m.deps;
  3. var allReady = deps.every(function(dep) {
  4. return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
  5. })
  6. if (deps.length === 0 || allReady) {
  7. return true;
  8. }
  9. };

  而盘活的契机放在script的onload函数中。一个script元素的生命周期为:

  创建元素-》加载脚本文件-》解析脚本文件(执行js代码)-》onload事件-》销毁

  所以如果onload中模块是阵眼模块,或者依赖模块已被全部加载完毕,则执行factory函数。然后循环检查依赖,一层一层的盘活其他依赖网。

  1. script.onload = function() {
  2. var module = modules[url];
  3. if (module && isReady(module) && loadings.indexOf(url) > -1) {
  4. callFactory(module);
  5. }
  6. checkDeps();
  7. };

  整个加载器代码如下:

  1. (function(global){
  2. global.$ = {
  3. log: function(m) {
  4. console.log(m);
  5. }
  6. };
  7. global = global || window;
  8. modules = {};
  9. loadings = [];
  10. loadedJs = [];
  11. //module: id, state, factory, result, deps;
  12. global.require = function(deps, callback, parent){
  13. var id = parent || "Bodhi" + Date.now();
  14. var cn = 0, dn = deps.length;
  15. var args = [];
  16.  
  17. var module = {
  18. id: id,
  19. deps: deps,
  20. factory: callback,
  21. state: 1,
  22. result: null
  23. };
  24. modules[id] = module;
  25.  
  26. deps.forEach(function(dep) {
  27. if (modules[dep] && modules[dep].state === 2) {
  28. cn++
  29. args.push(modules[dep].result);
  30. } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
  31. loadJS(dep);
  32. loadedJs.push(dep);
  33. }
  34. });
  35. if (cn === dn) {
  36. callFactory(module);
  37. } else {
  38. //loadJS(id);// require只是用来加载其他模块的
  39. loadings.push(id);
  40. checkDeps();
  41. }
  42. };
  43.  
  44. global.define = function(deps, callback) {
  45. var id = getCurrentScript();
  46. if (modules[id]) {
  47. console.error('multiple define module: ' + id);
  48. }
  49.  
  50. require(deps, callback, id);
  51. };
  52.  
  53. function loadJS(url) {
  54. var script = document.createElement('script');
  55. script.type = "text/javascript";
  56. script.src = url + '.js';
  57. script.onload = function() {
  58. var module = modules[url];
  59. if (module && isReady(module) && loadings.indexOf(url) > -1) {
  60. callFactory(module);
  61. }
  62. checkDeps();
  63. };
  64. var head = document.getElementsByTagName('head')[0];
  65. head.appendChild(script);
  66. };
  67.  
  68. function checkDeps() {
  69. for (var p in modules) {
  70. var module = modules[p];
  71. if (isReady(module) && loadings.indexOf(module.id) > -1) {
  72. callFactory(module);
  73. checkDeps(); // 如果成功,在执行一次,防止有些模块就差这次模块没有成功
  74. }
  75. }
  76. };
  77.  
  78. function isReady(m) {
  79. var deps = m.deps;
  80. var allReady = deps.every(function(dep) {
  81. return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
  82. })
  83. if (deps.length === 0 || allReady) {
  84. return true;
  85. }
  86. };
  87.  
  88. function callFactory(m) {
  89. var args = [];
  90. for (var i = 0, len = m.deps.length; i < len; i++) {
  91. args.push(modules[m.deps[i]].result);
  92. }
  93. m.result = m.factory.apply(window, args);
  94. m.state = 2;
  95.  
  96. var idx = loadings.indexOf(m.id);
  97. if (idx > -1) {
  98. loadings.splice(idx, 1);
  99. }
  100. };
  101.  
  102. function getCurrentScript(base) {
  103. // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
  104. var stack;
  105. try {
  106. a.b.c(); //强制报错,以便捕获e.stack
  107. } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
  108. stack = e.stack;
  109. if (!stack && window.opera) {
  110. //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
  111. stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
  112. }
  113. }
  114. if (stack) {
  115. /**e.stack最后一行在所有支持的浏览器大致如下:
  116. *chrome23:
  117. * at http://113.93.50.63/data.js:4:1
  118. *firefox17:
  119. *@http://113.93.50.63/query.js:4
  120. *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
  121. *@http://113.93.50.63/data.js:4
  122. *IE10:
  123. * at Global code (http://113.93.50.63/data.js:4:1)
  124. * //firefox4+ 可以用document.currentScript
  125. */
  126. stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
  127. stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符
  128. return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置
  129. }
  130. var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找
  131. for (var i = nodes.length, node; node = nodes[--i]; ) {
  132. if ((base || node.className === moduleClass) && node.readyState === "interactive") {
  133. return node.className = node.src;
  134. }
  135. }
  136. };
  137. })(window)

  测试代码:

  1. <!DOCTYPE HTML>
  2. <html>
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
  6. <meta http-equiv="X-UA-Compatible" content="IE=EDGE" />
  7. <title>Web AppBuilder for ArcGIS</title>
  8. <link rel="shortcut icon" href="builder/images/shortcut.png">
  9. </head>
  10. <body class="claro">
  11. <script src="./loader.js"></script>
  12. <script>
  13. require([
  14. 'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb',
  15. 'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa.bbb.ccc',
  16. 'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc',
  17. 'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ddd',
  18. 'http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff'], function(aaabbbccc){
  19. console.log('simple loader');
  20. console.log(arguments);
  21. });
  22. </script>
  23. </body>
  24. </html>
  1. define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa",
  2. "http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc"],function(a, c){
  3. console.log("已加载bbb模块", 7)
  4. return {
  5. aaa: a,
  6. ccc: c.ccc,
  7. bbb: "bbb"
  8. }
  9. })

bbb

  1. define([], function(){
  2. console.log("已加载aaa.bbb.ccc模块", 7)
  3. return "aaa.bbb.ccc";
  4. });

aaa.bbb.ccc

  1. define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(aaa){
  2. $.log("已加载ccc模块")
  3. return {
  4. aaa: aaa,
  5. ccc: "ccc555"
  6. }
  7. })

ccc

  1. define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa",
  2. "http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/bbb",
  3. "http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/ccc",
  4. "http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/fff"],function(a,b,c,f){
  5. $.log("已加载ddd模块", 7);
  6. return {
  7. bbb: b,
  8. ddd: "ddd",
  9. length: arguments.length
  10. }
  11. })

ddd

  1. define(['http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/more/ggg'], function(g){
  2. $.log("已加载fff模块")
  3. return {
  4. ggg: g,
  5. fff: "fff"
  6. }
  7. })

fff

  1. define([], function(){
  2. console.log("已加载aaa模块", 7)
  3. return "aaa"
  4. });

aaa

  1. define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/more/ggg"],function(ret){
  2. $.log("已加载eee模块",7)
  3. return {
  4. eee: "eee",
  5. aaa: ret.aaa,
  6. ggg: ret.ggg
  7. }
  8. })

eee

  1. define(["http://lzz-pc.chn.esri.com/arcgis/apps/webappbuilder/mass/simpleAMDLoader/aaa"],function(a){
  2. $.log("已加载ggg模块",7)
  3. return {
  4. aaa: a,
  5. ggg:"ggg"
  6. }
  7. })

ggg

  执行结果如下:

  1. 已加载aaa模块 7
  2. loader.js:4 已加载ggg模块
  3. loader.js:4 已加载fff模块
  4. aaa.bbb.ccc.js:2 已加载aaa.bbb.ccc模块 7
  5. loader.js:4 已加载ccc模块
  6. bbb.js:3 已加载bbb模块 7
  7. loader.js:4 已加载ddd模块
  8. index.html:19 simple loader
  9. index.html:20 Arguments[5]

  下一篇文章将会为我们的加载器加上模块路径解析功能,到时候我们便不用书写如此丑陋的模块id了。

  

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

AMD加载器实现笔记(一)的更多相关文章

  1. AMD加载器实现笔记(二)

    AMD加载器实现笔记(一)中,我们实现了一个简易的模块加载器.但到目前为止这个加载器还并不能称为AMD加载器,原因很简单,我们还不支持AMD规范中的config配置.这篇文章中我们来添加对config ...

  2. AMD加载器实现笔记(五)

    前几篇文章对AMD规范中的config属性几乎全部支持了,这一节主要是进一步完善.到目前为止我们的加载器还无法处理环形依赖的问题,这一节就是解决环形依赖. 所谓环形依赖,指的是模块A的所有依赖项的依赖 ...

  3. AMD加载器实现笔记(四)

    继续这一系列的内容,到目前为止除了AMD规范中config的map.config参数外,我们已经全部支持其他属性了.这一篇文章中,我们来为增加对map的支持.同样问题,想要增加map的支持首先要知道m ...

  4. AMD加载器实现笔记(三)

    上一篇文章中我们为config添加了baseUrl和packages的支持,那么这篇文章中将会看到对shim与paths的支持. 要添加shim与paths,第一要务当然是了解他们的语义与用法.先来看 ...

  5. AngularJs2与AMD加载器(dojo requirejs)集成

    现在是西太平洋时间凌晨,这个问题我鼓捣了一天,都没时间学英语了,英语太差,相信第二天我也看不懂了,直接看结果就行. 核心原理就是require在AngularJs2编译过程中是关键字,而在浏览器里面运 ...

  6. Promise实现简易AMD加载器

    在最新的Chrome和FF中已经 实现了Promise.有了Promise我们用数行代码即可实现一个简易AMD模式的加载器 var registry = { promises: { }, resolv ...

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

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

  8. KnockoutJS 3.X API 第六章 组件(5) 高级应用组件加载器

    无论何时使用组件绑定或自定义元素注入组件,Knockout都将使用一个或多个组件装载器获取该组件的模板和视图模型. 组件加载器的任务是异步提供任何给定组件名称的模板/视图模型对. 本节目录 默认组件加 ...

  9. 构建服务端的AMD/CMD模块加载器

    本文原文地址:http://trock.lofter.com/post/117023_1208040 . 引言:  在前端开发领域,相信大家对AMD/CMD规范一定不会陌生,尤其对requireJS. ...

随机推荐

  1. iOS TableView如何刷新指定的cell或section

    指定的section单独刷新 NSIndexSet *indexSet=[[NSIndexSet alloc]initWithIndex:indexPath.row]; [tableview relo ...

  2. 图表插件使用汇总(echarts,highchairts)

    1.echarts之饼图显示数字 option={ title: { text: '某站点用户访问来源', subtext: '纯属虚构', x: 'center' }, tooltip: { tri ...

  3. 按Enter键触发事件

    1.document.onkeydown=function(e){        var keycode=document.all?event.keyCode:e.which;        if(k ...

  4. js动态时间

    一.在<head></head> 之间写入下面js代码 <script type="text/javascript" language="J ...

  5. hdoj 2022 海选女主角

    Problem Description potato老师虽然很喜欢教书,但是迫于生活压力,不得不想办法在业余时间挣点外快以养家糊口.“做什么比较挣钱呢?筛沙子没力气,看大门又不够帅...”potato ...

  6. linux下安装oracle

    一>1.关闭防火墙,禁用selinux vi /etc/selinux/config  修改SELINUX=disabled,然后重启,如果不想重启使用命令setenforce 0 2.安装依赖 ...

  7. js原生实现选项卡功能

    选项卡在js中是一个重要的知识点.他没有那么难,但在工作中却有重要的位置.几乎在每一个网站都能看到选项卡的实例.所以今天写一下选项卡的实现. 我们设想有三个按钮分别来控制三个盒子当我们点击当前的按钮的 ...

  8. MySQL中GROUP_CONCAT中排序

    SELECT concat('',group_concat(option_name  )) as option_name,select_id            FROM zuyi_t_search ...

  9. javaWeb实现文件上传与下载 (转)

    文件上传概述 实现web开发中的文件上传功能,需完成如下二步操作: 在web页面中添加上传输入项 在servlet中读取上传文件的数据,并保存到本地硬盘中. 如何在web页面中添加上传输入项? < ...

  10. 开园第一篇 - 论移动开发环境 IOS与Android的差异

    首先,在真正写技术之前做个自我简介.本人08年开始学c语言 一年后,转vc++.开始接触MFC MFC做了两年.转眼11年了我考上了一个不知名的大专.搞C++发现没有市场了因为当时酷狗腾讯的软件已经日 ...