javascript【AMD模块加载器】浅析V2(整合DOM ready)
如果你还不了解AMD模块加载器,可以先看看我的前一篇文章,之后又在其基础上做了一点小修改。
主要是修改了检测循环依赖的函数,不在遍历对象。
第二就是加入了模块配置方法。以及将lynxcat更名为lynx。
还有就是为模块加入了dom ready机制。
var ready = function (){ var isReady = false; var readyList = []; var ready = function(fn){ if(isReady){ fn(); }else{ readyList.push(fn); } }; var fireReady = function(){ for(var i = 0,len = readyList.length; i < len; i++){ readyList[i](); } readyList = []; lynx.modules.ready.state = 2; checkLoadReady(); }; var bindReady = function(){ if(isReady){ return; } isReady=true; fireReady(); if(doc.removeEventListener){ doc.removeEventListener("DOMContentLoaded",bindReady,false); }else if(doc.attachEvent){ doc.detachEvent("onreadystatechange", bindReady); } }; if( doc.readyState === "complete" ) { bindReady(); }else if(doc.addEventListener){ doc.addEventListener("DOMContentLoaded", bindReady, false); }else if(doc.attachEvent){ doc.attachEvent("onreadystatechange", function(){ if((/loaded|complete/).test(doc.readyState)){ bindReady(); } }); (function(){ if(isReady){ return; } var node = new Image(); var timer = setInterval(function(){ try{ isReady || node.doScroll('left'); node = null; }catch(e){ return; } clearInterval(timer); bindReady(); }, 16); }()); } return ready; }()
关于ready方法,其实很简单。 首先是提供一个方法,把用户的ready的方法存入一个callbacks数组里。 然后在检测到dom ready的时候执行callbacks里面的数组,如果是在dom ready之后传入ready的callback就直接执行。 具体检测dom ready就需要看浏览器的支持了,如果是支持标准w3c规范的浏览器DOMContentLoaded 方法,IE则支持onreadystatechange方法。 还可以利用的一个特性就是 doScroll要求最初的文档被完全加载。否则就会出错。 这样一来我们就可以用setInterval方法来不断的检测doScroll是否出错,如果不出错。那么代表文档已经加载完成了。 总体来说ready方法还是很简单的。
然后说说如何把ready整合到模块中。 首先我想到的是,在require中判断是否已经dom ready如果没有就把当前的require传入到ready方法。 也就是说,这个方法是让页面ready完成后在加载模块。 然后这样有一个坏处就是没有充分利用dom ready的这段时间。 后来看了一下别人的实现,又自己思索了一下。 决定把ready作为一个AMD模块放入到框架中。这样一来就就可以用模块加载的原理来处理了。 如果某个模块依赖ready,就一定要在ready之后才执行它的factory方法。 具体实现的方式是将ready作为一个基准模块写入到modules中,不过它不用被加载。而是在检测dom ready之后就将modules中的ready状态改为2。并检测一下是否所有模块都被加载完毕。这样一来就可以将dom加载的这段时间用来加载模块了。
然而这样一来就又有一个新的bug在里面,就是如果用户真的想加载一个ready方法的时候就会被误认为是dom ready。 关于这点目前还没想到什么办法,不过用户可以通过写url而不是写ready这样的名字来加载。 在下一版的加载器中我会加入部模块自动加前缀来解决这个问题。还有会加入控制并发数。以及对模块合并的简单说明。
以下是全部源码。
;(function(win, undefined){ win = win || window; var doc = win.document || document, head = doc.head || doc.getElementsByTagName("head")[0], hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice, configure = {}, basePath = (function(nodes){ var node, url; if(configure.baseUrl){ node = nodes[nodes.length - 1]; url = (node.hasAttribute ? node.src : node.getAttribute("src", 4)).replace(/[?#].*/, ""); }else{ url = configure.baseUrl; } return url.slice(0, url.lastIndexOf('/') + 1); }(doc.getElementsByTagName('script'))), _lynx = win.lynx; /** * 框架入口 */ function lynx(exp, context){ return new lynx.prototype.init(exp, context); } lynx.prototype = { constructor : lynx, /** * 初始化 * @param {All} expr * @param {All} context * @return {Object} * */ init : function(expr, context){ if(typeof expr === 'function'){ require('ready', expr); } //TODO } } lynx.fn = lynx.prototype.init.prototype = lynx.prototype; /** * 继承方法 */ lynx.fn.extend = lynx.extend = function(){ var args = slice.call(arguments), deep = typeof args[args.length - 1] == 'bollean' ? args.pop() : false; if(args.length == 1){ args[1] = args[0]; args[0] = this; args.length = 2; } var target = args[0], i = 1, len = args.length, source, prop; for(; i < len; i++){ source = args[i]; for(prop in source){ if(hasOwn.call(source, prop)){ if(typeof source[prop] == 'object'){ target[prop] = {}; this.extend(target[prop],source[prop]); }else{ if(target[prop] === undefined){ target[prop] = source[prop]; }else{ deep && (target[prop] = source[prop]); } } } } } }; /** * mix * @param {Object} target 目标对象 * @param {Object} source 源对象 * @return {Object} 目标对象 */ lynx.mix = function(target, source){ if( !target || !source ) return; var args = slice.call(arguments), i = 1, override = typeof args[args.length - 1] === "boolean" ? args.pop() : true, prop; while ((source = args[i++])) { for (prop in source) { if (hasOwn.call(source, prop) && (override || !(prop in target))) { target[prop] = source[prop]; } } } return target; }; lynx.mix(lynx, { modules : { //保存加载模块 ready : { state : 1, exports : lynx } }, moduleCache : {}, //正在加载的队列缓存 loadings : [], //getCurrentScript取不到值的时候用来存储当前script onload的回调函数数组 /** * parse module * @param {String} id 模块名 * @param {String} basePath 基础路径 * @return {Array} */ parseModule : function(id, basePath){ var url, result, ret, dir, paths, i, len, ext, modname, protocol = /^(\w+\d?:\/\/[\w\.-]+)(\/(.*))?/; if(result = protocol.exec(id)){ url = id; paths = result[3] ? result[3].split('/') : []; }else{ result = protocol.exec(basePath); url = result[1]; paths = result[3] ? result[3].split('/') : []; modules = id.split('/'); paths.pop(); for(i = 0, len = modules.length; i < len; i++){ dir = modules[i]; if(dir == '..'){ paths.pop(); }else if(dir !== '.'){ paths.push(dir); } } url = url + '/' + paths.join('/'); } modname = paths[paths.length - 1]; ext = modname.slice(modname.lastIndexOf('.')); if(ext != '.js'){ url = url + '.js'; }else{ modname = modname.slice(0, modname.lastIndexOf('.')); } if(modname == ''){ modname = url; } return [modname, url]; }, /** * get uuid * @param {String} prefix * @return {String} uuid */ guid : function(prefix){ prefix = prefix || ''; return prefix + (+new Date()) + String(Math.random()).slice(-8); }, /** * noop 空白函数 */ noop : function(){ }, /** * error * @param {String} str */ error : function(str){ throw new Error(str); }, /** * @return {Object} lynx */ noConflict : function(deep) { if ( window.lynx === lynx ) { window.lynx = _lynx; } return lynx; } }); //================================ 模块加载 ================================ /** * 模块加载方法 * @param {String|Array} ids 需要加载的模块 * @param {Function} callback 加载完成之后的回调 * @param {String} parent 父路径 */ win.require = lynx.require = function(ids, callback, parent){ ids = typeof ids === 'string' ? [ids] : ids; var i = 0, len = ids.length, flag = true, uuid = parent || lynx.guid('cb_'), path = parent || basePath, modules = lynx.modules, moduleCache = lynx.moduleCache, args = [], deps = {}, id, result; for(; i < len; i++){ id = ids[i]; if(id == 'ready'){ result = ['ready','ready']; }else{ result = lynx.parseModule(id, path); } if(!deps[result[1]]){ //减少检测重复依赖的次数 if(checkCircularDeps(uuid, result[1])){ lynx.error('模块[url:'+ uuid +']与模块[url:'+ result[1] +']循环依赖'); } deps[result[1]] = 'lynx'; } args.push(result[1]); if(!modules[result[1]]){ modules[result[1]] = { name : result[0], url : result[1], state : 0, exports : {} } flag = false; lynx.loadJS(result[1]); }else if(modules[result[1]].state != 2){ flag = false; } } moduleCache[uuid] = { uuid : uuid, factory : callback, args : args, deps : deps, state : 1 } if(flag){ fireFactory(uuid); return checkLoadReady(); } }; /** * @param {String} id 模块名 * @param {String|Array} [dependencies] 依赖列表 * @param {Function} factory 工厂方法 */ win.define = function(id, dependencies, factory){ if(typeof dependencies === 'function'){ factory = dependencies; if(typeof id === 'array'){ dependencies = id; }else if(typeof id === 'string'){ dependencies = []; } }else if (typeof id == 'function'){ factory = id; dependencies = []; } id = lynx.getCurrentScript(); if(!id){ lynx.loadings.push(function(id){ require(dependencies, factory, id); }); }else{ require(dependencies, factory, id); } } require.amd = define.amd = lynx.modules; /** * fire factory * @param {String} uuid */ function fireFactory(uuid){ var moduleCache = lynx.moduleCache, modules = lynx.modules, data = moduleCache[uuid], deps = data.args, result, i = 0, len = deps.length, args = []; for(; i < len; i++){ args.push(modules[deps[i]].exports) } result = data.factory.apply(null, args); if(modules[uuid]){ modules[uuid].state = 2; modules[uuid].exports = result; delete moduleCache[uuid]; }else{ delete lynx.moduleCache; } return result; } /** * 检测是否全部加载完毕 */ function checkLoadReady(){ var moduleCache = lynx.moduleCache, modules = lynx.modules, data, prop, deps, mod; loop: for (prop in moduleCache) { data = moduleCache[prop]; deps = data.deps; for(mod in deps){ if(hasOwn.call(modules, mod) && modules[mod].state != 2){ continue loop; } } if(data.state != 2){ fireFactory(prop); checkLoadReady(); } } } /** * 检测循环依赖 * @param {String} id * @param {Array} dependencie */ function checkCircularDeps(id, dependencie){ var moduleCache = lynx.moduleCache, depslist = moduleCache[dependencie] ? moduleCache[dependencie].deps : {}; if(hasOwn.call(depslist, id) && depslist[id] == 'lynx'){ return true; } return false; } lynx.mix(lynx, { /** * 加载JS文件 * @param {String} url */ loadJS : function(url){ var node = doc.createElement("script"); node[node.onreadystatechange ? 'onreadystatechange' : 'onload'] = function(){ if(!node.onreadystatechange || /loaded|complete/i.test(node.readyState)){ var fn = lynx.loadings.pop(); fn && fn.call(null, node.src); lynx.modules[node.src] && lynx.modules[node.src].state == 1; node.onload = node.onreadystatechange = node.onerror = null; head.removeChild(node); } } node.onerror = function(){ lynx.error('模块[url:'+ node.src +']加载失败'); node.onload = node.onreadystatechange = node.onerror = null; head.removeChild(node); } node.src = url; head.insertBefore(node, head.firstChild); }, /** * get current script [此方法来自司徒正美的博客] * @return {String} */ getCurrentScript : function(){ //取得正在解析的script节点 if (doc.currentScript) { //firefox 4+ return doc.currentScript.src; } // 参考 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) */ stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分 stack = stack[0] === "(" ? stack.slice(1, -1) : stack; return stack.replace(/(:\d+)?:\d+$/i, ""); //去掉行号与或许存在的出错字符起始位置 } var nodes = head.getElementsByTagName("script"); //只在head标签中寻找 for (var i = 0, node; node = nodes[i++]; ) { if (node.readyState === "interactive") { return node.src; } } }, config : function(option){ lynx.mix(configure, option); }, //============================== DOM Ready ============================= /** * dom ready * @param {Function} callback */ ready : function (){ var isReady = false; var readyList = []; var ready = function(fn){ if(isReady){ fn(); }else{ readyList.push(fn); } }; var fireReady = function(){ for(var i = 0,len = readyList.length; i < len; i++){ readyList[i](); } readyList = []; lynx.modules.ready.state = 2; checkLoadReady(); }; var bindReady = function(){ if(isReady){ return; } isReady=true; fireReady(); if(doc.removeEventListener){ doc.removeEventListener("DOMContentLoaded",bindReady,false); }else if(doc.attachEvent){ doc.detachEvent("onreadystatechange", bindReady); } }; if( doc.readyState === "complete" ) { bindReady(); }else if(doc.addEventListener){ doc.addEventListener("DOMContentLoaded", bindReady, false); }else if(doc.attachEvent){ doc.attachEvent("onreadystatechange", function(){ if((/loaded|complete/).test(doc.readyState)){ bindReady(); } }); (function(){ if(isReady){ return; } var node = new Image(); var timer = setInterval(function(){ try{ isReady || node.doScroll('left'); node = null; }catch(e){ return; } clearInterval(timer); bindReady(); }, 16); }()); } return ready; }() }); win.lynx = lynx; }(window));
使用方法
//hello.js文件 define('hello', 'test',function(test){ return {world : 'hello, world!' + test}; }); //test.js文件 define('test', function(){ return 'this is test'; }); //主文件 lynxcat.require('ready','hello',function(hello){ console.log(hello.world); //hello, world!this is test; });