本版本最大的改进就引入强大的调试机制。如果一个框架使用了模块加载后,迎来的最大问题莫过于调试。由于有了模块化,因此不需要担忧体积的问题,因此大放手脚伸入前端各个领域去,JS文件暴涨,也意味着API数量瀑涨,就像jQuery那一点儿API有的人都记不全,更别说像EXT,YUI,dojo这样的巨无霸了。对于这个方法是在A模块还是B模块,我们在调用时还可以查一查,但出错时,就未必出A模块或B模块内,A模块还可能依赖于C模块与D模块,D模块还有依赖,这样一级级下去,我们很难追溯到出错的源头。特别是,如果这个JS文件是动态加载的,然后又删掉了,连firebug也查不了!因此强化调试机制势在必行,这也是本版本最大的亮点!
本版本的其他改进:
- 升级UUID系统,以便页面出现多个版本共存时,让它们共享一个计数器。
- 简化_checkFail方法,如果出现死链接,直接打印模块名便是,不用再放入错误栈中了。
- 简化deferred列队,统一先进先出。
- 改进$.mix方法,允许只存在一个参数,直接将属性添加到$命名空间上。
- 内部方法assemble更名为setup,并强化调试机制,每加入一个新模块, 都会遍历命名空间与原型上的方法,重写它们,添加try catch逻辑。
好了,我们再回到增强调试机制的话题上。
在mass Framework种子模块的require方法中这样一段代码:
if ( dn === cn ){ //在依赖都已执行过或没有依赖的情况下 if ( token && !( token in transfer ) ){ mapper[ token ].state = 2 //如果是使用合并方式,模块会跑进此分支(只会执行一次) return transfer[ token ] = setup( callback, args,token ); } else if ( !token ){ //普通的回调可执行无数次 return setup( callback, args,token ); } } |
反正如果某个JS文件没有死链,它对应的模块就肯定进入setup方法。setup 方法有三个参数,第一个模块名,字符串。第二个是它的依赖列表,一个字符串数组。最后一个是函数,可以是模块自身,也可以是用户的回调。情况与$.define方法一模一样,不同的是,这模块可能已经被修改过,加了文件夹名。
//收集依赖列表对应模块的返回值,传入目标模块中执行 function setup( name, deps, fn ){ for ( var i = 0,argv = [], d; d = deps[i++]; ) { argv.push( transfer[d] ); } var ret = fn.apply( global, argv ); if ($[ "@debug" ]){ //如果打开调试机制 for ( i in $){ debug($, i, name); } for ( i in $.fn){ debug($.fn, i, name,1); } } return ret; } |
setup的用法在注释中已说很清楚了,就行执行模块自身,以便为命名空间与mass的原型添加新的方法或属性。比如lang模块,它本身是没有依赖(特指标准浏览器下,IE下还要加lang_fix模块),它的模块函数执行后,$命名空间就多出isPlainObject, isNative, isEmptyObject, isArrayLike, format, tag, quote, dump, parseJS, parseJSON, parseXML, each, map ,isFunction, isArray, lang这些方法。并且返回$.lang这个方法,它会添加到内部的transfer对象,键名为其对应的模块。因此你看到setup方法存在transfer[d]这样的语句。
如果已打开调试机制(默认是打开的),它就会新加入模块的方法进行重写,这个由另一个内部方法去完成。
var rdebug = /^(init|constructor|lang|query)$|^is/ function debug(obj, name, module, p){ var fn = obj[name]; if ( typeof fn == "function" && !fn[ "@debug" ]){ if ( rdebug.test( name )){ fn[ "@debug" ] = name; } else { var method = obj[name] = function (){ try { return method[ "@debug" ].apply( this ,arguments) } catch (e){ $.log( "[[ " +module+ "::" +(p? "$.fn." : "$." )+name+ " ]] gone wrong" ); $.log(e); throw e; } } for ( var i in fn){ method[i] = fn[i]; } method[ "@debug" ] = fn; method.toString = function (){ return fn.toString() } method.valueOf = function (){ return fn.valueOf(); } } } } |
此方法目的是重写原方法,但为了节约性能,没有使用curry,而是将原方法放到新方法的一个叫“@debug”的属性上,使用在try catch中小心地调用它。如果出错就把预先写好的调试消息打印出来。为了防止此方法人工添加过各种自定义属性,我们有必要把这些私有属性再for in 一下,转移到新方法上。但for in在IE678下有BUG,因此还需要单独处理一下valueOf与toString方法。此外,对于一些核心方法与调用异常频繁的方法就不重写了, 如isXXX系列,init构造器,query选择器,lang语言链……基本上它们都有足够强悍的代码防御,用不着多此一举!
说了这么多理论,让我们看看实际效果。比如说我们想调用lang模块的format方法,它有两种传参方式,但无论哪一种,第一个参数总是字符串!
$.require( "ready,lang" , function (){ var a = $.format( "Result is #{0},#{1}" , 22,33); alert(a); //"Result is 22,33" }) |
但你如果误传了一个数组进去,立马报错,在各浏览器显示如下(这是未开启调试机制的情况)
$.require( "ready,lang" , function (){ $.format([]); }) |
除了opera可以看出一些眉目外,其他浏览器的调试消息基本没有用。现在只有一行代码,我们当然猜得出是format出了问题,如果多几行就惨了。
我们再看看打开调试机制之后的样子,它就比原来多一行有用的消息。
@lang表示它所在的模块,按照mass Framework的文件名与模块名一一对应的机制,不难知道是lang.js这个文件有错,如果是@more/aaa,则是more目录下的aaa.js文件有错。$.format就是出错的方法名,这个用IDE定位一下立即就找到了,然后根据浏览器原有消息与方法源码,应该很快就找到原因。
//mass Framework的模块加载,创建元素,插入元素,绑定事件 $.require( "ready,event" , function (){ $( "body" ).append( "<div>新节点</div>" ).on( "click" , "div" , function (){ alert( this .innerHTML) }) }) |
基本上就是这样,源码放在github上,欢迎试用。