
AMD加载器实现笔记(五)
View Code
发布日期:2021-05-09 02:06:56
浏览次数:21
分类:原创文章
本文共 15073 字,大约阅读时间需要 50 分钟。
前几篇文章对AMD规范中的config属性几乎全部支持了,这一节主要是进一步完善。到目前为止我们的加载器还无法处理环形依赖的问题,这一节就是解决环形依赖。
所谓环形依赖,指的是模块A的所有依赖项的依赖中有没有依赖A模块本身的模块。如果有那就说明存在环形依赖。所以检验的方式是利用递归,检查一个模块的依赖的依赖项中有没有依赖A模块,以及依赖项的依赖项的依赖项中有没有A模块,核心代码如下:
function checkCircleRef(start, target){ var m = modules[start]; if (!m) { return false; } var depModules = m.deps.map(function(dep) { return modules[dep] || null; }); return depModules.some(function(m) {//检查依赖项的依赖项 if (!m) { return false; } return m.deps.some(function(dep) { var equal = dep === target; if (equal) { console.error("circle reference: ", target, m.id); } return equal; }); }) ? true : depModules.some(function(m) {//检查依赖项的依赖项的依赖项 if (!m) { return false; } return m.deps.some(function(dep) { return checkCircleRef(dep, target); }); }); };
剩下的问题是我们把检查放到哪里去?我们的模块最先在require中注册,所以最佳的位置是放在require函数中去检查:
//require 函数//。。。。。。。var module = { id: id, deps: deps, factory: callback, state: 1, result: null }; modules[id] = module; if (checkCircleRef(id, id)) { hasCircleReferece = true; return; }// ......................//.......................//......................
下一个要面临的问题就是:如果存在依赖项如何处理?对此,我的做法是如果存在环形依赖,结束整个加载过程。我们在加载器内部使用一个哨兵变量,一旦存在环形依赖,停止所有工作。如:loadJs中:
script.onload = function() { if (hasCircleReferece) { return; } var module = modules[url]; if (module && isReady(module) && loadings.indexOf(url) > -1) { callFactory(module); } checkDeps(); };
如define函数中:
if (modules[id]) { console.error('multiple define module: ' + id); } if (!hasCircleReferece) { require(deps, callback, id); }
测试:
require.config({ baseUrl: "./", packages: [{ name: "more", location: "./more" }, { name: "mass", location: "../" }, { name: "wab", location: "../../../" }], shim: { "something": { "deps": ['jquery'], exports: 'something', init: function(jq, ol) { console.log(jq); console.log($); return something + " in shim"; } } }, map: { '*': { 'jquery': 'jquery-private' }, 'jquery-private': { 'jquery': 'jquery' } }, paths: { 'jquery': "../../Bodhi/src/roots/jquery" } }); require([ 'bbb', 'aaa.bbb.ccc', 'ccc', 'ddd', 'fff', 'something' ], function(aaabbbccc){ console.log('simple loader'); console.log(arguments); });
bbb中:
define(["aaa","ccc"],function(a, c){ console.log("已加载bbb模块", 7) return { aaa: a, ccc: c.ccc, bbb: "bbb" }})
aaa中:
define(['bbb'], function(){ console.log("已加载aaa模块", 7) return "aaa"});
测试结果:
circle reference: simpleAMDLoader/aaa simpleAMDLoader/bbb
目前为止整个加载器代码如下:
1 (function(global){ 2 global = global || window; 3 var modules = {}; 4 var loadings = []; 5 var loadedJs = []; 6 var hasCircleReferece = false; 7 //module: id, state, factory, result, deps; 8 global.require = function(deps, callback, parent){ 9 var id = parent || "Bodhi" + Date.now(); 10 var cn = 0, dn = deps.length; 11 var args = []; 12 13 var oriDeps = deps.slice();//保留原始dep的模块Id 14 15 // dep为非绝对路径形式,而modules的key仍然需要绝对路径 16 deps = deps.map(function(dep) { 17 if (modules[dep]) { //jquery 18 return dep; 19 } else if (dep in global.require.parsedConfig.paths) { 20 return dep; 21 } 22 var rel = ""; 23 if (/^Bodhi/.test(id)) { 24 rel = global.require.parsedConfig.baseUrl; 25 } else { 26 var parts = parent.split('/'); 27 parts.pop(); 28 rel = parts.join('/'); 29 } 30 return getModuleUrl(dep, rel); 31 }); 32 33 var module = { 34 id: id, 35 deps: deps, 36 factory: callback, 37 state: 1, 38 result: null 39 }; 40 modules[id] = module; 41 42 if (checkCircleRef(id, id)) { 43 hasCircleReferece = true; 44 return; 45 } 46 //checkCircleRef(id, id) 47 48 deps.forEach(function(dep, i) { 49 if (modules[dep] && modules[dep].state === 2) { 50 cn++ 51 args.push(modules[dep].result); 52 } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) { 53 loadJS(dep, oriDeps[i]); 54 loadedJs.push(dep); 55 } 56 }); 57 if (cn === dn) { 58 callFactory(module); 59 } else { 60 loadings.push(id); 61 checkDeps(); 62 } 63 }; 64 65 global.require.config = function(config) { 66 this.parsedConfig = {}; 67 if (config.baseUrl) { 68 var currentUrl = getCurrentScript(); 69 var parts = currentUrl.split('/'); 70 parts.pop(); 71 var currentDir = parts.join('/'); 72 this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl); 73 } 74 var burl = this.parsedConfig.baseUrl; 75 // 得到baseUrl后,location相对baseUrl定位 76 this.parsedConfig.packages = []; 77 if (config.packages) { 78 for (var i = 0, len = config.packages.length; i < len; i++) { 79 var pck = config.packages[i]; 80 var cp = { 81 name: pck.name, 82 location: getRoute(burl, pck.location) 83 } 84 this.parsedConfig.packages.push(cp); 85 } 86 } 87 88 89 this.parsedConfig.paths = {}; 90 if (config.paths) { 91 for (var p in config.paths) { 92 this.parsedConfig.paths[p] = /^http(s)?/.test(config.paths[p]) ? config.paths[p] : getRoute(burl, config.paths[p]); 93 } 94 } 95 96 this.parsedConfig.map = {}; 97 if (config.map) { 98 this.parsedConfig.map = config.map; 99 }100 101 this.parsedConfig.shim = {};102 //shim 要放在最后处理103 if (config.shim) {104 this.parsedConfig.shim = config.shim;105 for (var p in config.shim) {106 var item = config.shim[p];107 define(p, item.deps, function() {108 var exports;109 if (item.init) {110 exports = item.init.apply(item, arguments);111 }112 113 return exports ? exports : item.exports;114 });115 }116 }117 118 console.log(this.parsedConfig);119 }120 121 global.define = function(id, deps, callback) {122 //加上moduleId的支持123 if (typeof id !== "string" && arguments.length === 2) {124 callback = deps;125 deps = id;126 id = "";127 }128 var id = id || getCurrentScript();129 130 var mId = getModuleId(id);131 if (mId || id in require.parsedConfig.shim) {132 mId = mId ? mId : id;133 var maping = getMapSetting(mId);134 135 if (maping) {136 deps = deps.map(function(dep) {137 return maping[dep] || dep;138 });139 }140 }141 if (modules[id]) {142 console.error('multiple define module: ' + id);143 }144 145 if (!hasCircleReferece) {146 require(deps, callback, id);147 }148 };149 150 global.define.amd = {};//AMD规范151 152 function getModuleId(url) {153 var script = document.querySelector('script[src="' + url + '"]');154 if (script) {155 return script.getAttribute('data-moduleId');156 } else {157 return null;158 }159 };160 161 function getMapSetting(mId) {162 if (mId in require.parsedConfig.map) {163 return require.parsedConfig[mId];164 } else if ('*' in require.parsedConfig.map) {165 return require.parsedConfig.map['*'];166 } else {167 return null;168 }169 };170 171 function checkCircleRef(start, target){172 var m = modules[start];173 if (!m) {174 return false;175 }176 var depModules = m.deps.map(function(dep) {177 return modules[dep] || null;178 });179 180 181 return depModules.some(function(m) {182 if (!m) {183 return false;184 }185 return m.deps.some(function(dep) {186 var equal = dep === target;187 if (equal) {188 console.error("circle reference: ", target, m.id);189 }190 191 return equal;192 });193 }) ? true : depModules.some(function(m) {194 if (!m) {195 return false;196 }197 return m.deps.some(function(dep) {198 return checkCircleRef(dep, target);199 });200 });201 };202 203 function getRoute(base, target) {204 var bts = base.replace(/\/$/, "").split('/'); //base dir205 var tts = target.split('/'); //target parts206 while (isDefined(tts[0])) {207 if (tts[0] === '.') {208 return bts.join('/') + '/' + tts.slice(1).join('/');209 } else if (tts[0] === '..') {210 bts.pop();211 tts.shift();212 } else {213 return bts.join('/') + '/' + tts.join('/');214 }215 }216 };217 218 function isDefined(v) {219 return v !== null && v !== undefined;220 };221 222 function getModuleUrl(moduleId, relative) {223 function getPackage(nm) {224 for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) {225 var pck = require.parsedConfig.packages[i];226 if (nm === pck.name) {227 return pck;228 }229 }230 return false;231 }232 var mts = moduleId.split('/');233 var pck = getPackage(mts[0]);234 if (pck) {235 mts.shift();236 return getRoute(pck.location, mts.join('/'));237 } else if (mts[0] === '.' || mts[0] === '..') {238 return getRoute(relative, moduleId);239 } else {240 return getRoute(require.parsedConfig.baseUrl, moduleId);241 }242 };243 244 function loadJS(url, mId) {245 var script = document.createElement('script');246 script.setAttribute('data-moduleId', mId); //为script元素保留原始模块Id247 script.type = "text/javascript";248 //判断模块是否在paths中定义了路径249 script.src = (url in global.require.parsedConfig.paths ? global.require.parsedConfig.paths[url] : url) + '.js';250 script.onload = function() {251 if (hasCircleReferece) {252 return;253 }254 var module = modules[url];255 if (module && isReady(module) && loadings.indexOf(url) > -1) {256 callFactory(module);257 }258 checkDeps();259 };260 var head = document.getElementsByTagName('head')[0];261 head.appendChild(script);262 };263 264 function checkDeps() {265 for (var p in modules) {266 var module = modules[p];267 if (isReady(module) && loadings.indexOf(module.id) > -1) {268 callFactory(module);269 checkDeps(); // 如果成功,在执行一次,防止有些模块就差这次模块没有成功270 }271 }272 };273 274 function isReady(m) {275 var deps = m.deps;276 var allReady = deps.every(function(dep) {277 return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;278 })279 if (deps.length === 0 || allReady) {280 return true;281 }282 };283 284 function callFactory(m) {285 var args = [];286 for (var i = 0, len = m.deps.length; i < len; i++) {287 args.push(modules[m.deps[i]].result);288 }289 m.result = m.factory.apply(window, args);290 m.state = 2;291 292 var idx = loadings.indexOf(m.id);293 if (idx > -1) {294 loadings.splice(idx, 1);295 }296 };297 298 function getCurrentScript(base) {299 // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js300 var stack;301 try {302 a.b.c(); //强制报错,以便捕获e.stack303 } catch (e) { //safari的错误对象只有line,sourceId,sourceURL304 stack = e.stack;305 if (!stack && window.opera) {306 //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取307 stack = (String(e).match(/of linked script \S+/g) || []).join(" ");308 }309 }310 if (stack) {311 /**e.stack最后一行在所有支持的浏览器大致如下:312 *chrome23:313 * at http://113.93.50.63/data.js:4:1314 *firefox17:315 *@http://113.93.50.63/query.js:4316 *opera12:http://www.oldapps.com/opera.php?system=Windows_XP317 *@http://113.93.50.63/data.js:4318 *IE10:319 * at Global code (http://113.93.50.63/data.js:4:1)320 * //firefox4+ 可以用document.currentScript321 */322 stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分323 stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符324 return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置325 }326 var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找327 for (var i = nodes.length, node; node = nodes[--i]; ) {328 if ((base || node.className === moduleClass) && node.readyState === "interactive") {329 return node.className = node.src;330 }331 }332 };333 })(window)