seajs学习之模块的依赖加载及模块API的导出
前言
SeaJS非常强大,SeaJS可以加载任意JavaScript模块和css模块样式,SeaJS会保证你在使用一个模块时,已经将所依赖的其他模块载入到脚本运行环境中。
通过参照上文的demo,我们结合源码分析在简单的API调用的背后,到底使用了什么技巧来实现各个模块的依赖加载以及模块API的导出。
模块类和状态类
首先定义了一个Module类,对应与一个模块
functionModule(uri,deps){ this.uri=uri this.dependencies=deps||[] this.exports=null this.status=0 //Whodependsonme this._waitings={} //Thenumberofunloadeddependencies this._remain=0 }
Module有一些属性,uri对应该模块的绝对url,在Module.define函数中会有介绍;dependencies为依赖模块数组;exports为导出的API;status为当前的状态码;_waitings对象为当前依赖该模块的其他模块哈希表,其中key为其他模块的url;_remain为计数器,记录还未加载的模块个数。
varSTATUS=Module.STATUS={ //1-The`module.uri`isbeingfetched FETCHING:1, //2-ThemetadatahasbeensavedtocachedMods SAVED:2, //3-The`module.dependencies`arebeingloaded LOADING:3, //4-Themodulearereadytoexecute LOADED:4, //5-Themoduleisbeingexecuted EXECUTING:5, //6-The`module.exports`isavailable EXECUTED:6 }
上述为状态对象,记录模块的当前状态:模块初始化状态为0,当加载该模块时,为状态fetching;模块加载完毕并且缓存在cacheMods后,为状态saved;loading状态意味着正在加载该模块的其他依赖模块;loaded表示所有依赖模块加载完毕,执行该模块的回调函数,并设置依赖该模块的其他模块是否还有依赖模块未加载,若加载完毕执行回调函数;executing状态表示该模块正在执行;executed则是执行完毕,可以使用exports的API。
模块的定义
commonJS规范规定用define函数来定义一个模块。define可以接受1,2,3个参数均可,不过对于Module/wrappings规范而言,module.declare或者define函数只能接受一个参数,即工厂函数或者对象。不过原则上接受参数的个数并没有本质上的区别,只不过库在后台给额外添加模块名。
seajs鼓励使用define(function(require,exports,module){})这种模块定义方式,这是典型的Module/wrappings规范实现。但是在后台通过解析工厂函数的require方法来获取依赖模块并给模块设置id和url。
//Defineamodule Module.define=function(id,deps,factory){ varargsLen=arguments.length //define(factory) if(argsLen===1){ factory=id id=undefined } elseif(argsLen===2){ factory=deps //define(deps,factory) if(isArray(id)){ deps=id id=undefined } //define(id,factory) else{ deps=undefined } } //Parsedependenciesaccordingtothemodulefactorycode //如果deps为非数组,则序列化工厂函数获取入参。 if(!isArray(deps)&&isFunction(factory)){ deps=parseDependencies(factory.toString()) } varmeta={ id:id, uri:Module.resolve(id),//绝对url deps:deps, factory:factory } //TrytoderiveuriinIE6-9foranonymousmodules //导出匿名模块的uri if(!meta.uri&&doc.attachEvent){ varscript=getCurrentScript() if(script){ meta.uri=script.src } //NOTE:Iftheid-derivingmethodsaboveisfailed,thenfallsback //touseonloadeventtogettheuri } //Emit`define`event,usedinnocacheplugin,seajsnodeversionetc emit("define",meta) meta.uri?Module.save(meta.uri,meta): //Saveinformationfor"saving"workinthescriptonloadevent anonymousMeta=meta }
模块定义的最后,通过Module.save方法,将模块保存到cachedMods缓存体中。
parseDependencies方法比较巧妙的获取依赖模块。他通过函数的字符串表示,使用正则来获取require(“…”)中的模块名。
varREQUIRE_RE=/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g varSLASH_RE=/\\\\/g functionparseDependencies(code){ varret=[] //此处使用函数序列化(传入的factory)进行字符串匹配,寻找require(“...”)的关键字 code.replace(SLASH_RE,"") .replace(REQUIRE_RE,function(m,m1,m2){ if(m2){ ret.push(m2) } }) returnret }
异步加载模块
加载模块可以有多种方式,xhr方式可以同步加载,也可以异步加载,但是存在同源问题,因此难以在此使用。另外scripttag方式在IE和现代浏览器下可以保证并行加载和顺序执行,scriptelement方式也可以保证并行加载但不保证顺序执行,因此这两种方式都可以使用。
在seajs中,是采用scriptelement方式来并行加载js/css资源的,并针对旧版本的webkit浏览器加载css做了hack。
functionrequest(url,callback,charset){ varisCSS=IS_CSS_RE.test(url) varnode=doc.createElement(isCSS?"link":"script") if(charset){ varcs=isFunction(charset)?charset(url):charset if(cs){ node.charset=cs } } //添加onload函数。 addOnload(node,callback,isCSS,url) if(isCSS){ node.rel="stylesheet" node.href=url } else{ node.async=true node.src=url } //ForsomecachecasesinIE6-8,thescriptexecutesIMMEDIATELYafter //theendoftheinsertexecution,souse`currentlyAddingScript`to //holdcurrentnode,forderivingurlin`define`call currentlyAddingScript=node //ref:#185&http://dev.jquery.com/ticket/2709 baseElement? head.insertBefore(node,baseElement): head.appendChild(node) currentlyAddingScript=null } functionaddOnload(node,callback,isCSS,url){ varsupportOnload="onload"innode //forOldWebKitandOldFirefox if(isCSS&&(isOldWebKit||!supportOnload)){ setTimeout(function(){ pollCss(node,callback) },1)//Beginafternodeinsertion return } if(supportOnload){ node.onload=onload node.onerror=function(){ emit("error",{uri:url,node:node}) onload() } } else{ node.onreadystatechange=function(){ if(/loaded|complete/.test(node.readyState)){ onload() } } } functiononload(){ //EnsureonlyrunonceandhandlememoryleakinIE node.onload=node.onerror=node.onreadystatechange=null //Removethescripttoreducememoryleak if(!isCSS&&!data.debug){ head.removeChild(node) } //Dereferencethenode node=null callback() } } //针对旧webkit和不支持onload的CSS节点判断加载完毕的方法 functionpollCss(node,callback){ varsheet=node.sheet varisLoaded //forWebKit<536 if(isOldWebKit){ if(sheet){ isLoaded=true } } //forFirefox<9.0 elseif(sheet){ try{ if(sheet.cssRules){ isLoaded=true } }catch(ex){ //Thevalueof`ex.name`ischangedfrom"NS_ERROR_DOM_SECURITY_ERR" //to"SecurityError"sinceFirefox13.0.ButFirefoxislessthan9.0 //inhere,Soitisoktojustrelyon"NS_ERROR_DOM_SECURITY_ERR" if(ex.name==="NS_ERROR_DOM_SECURITY_ERR"){ isLoaded=true } } } setTimeout(function(){ if(isLoaded){ //Placecallbackheretogivetimeforstylerendering callback() } else{ pollCss(node,callback) } },20) }
其中有些细节还需注意,当采用scriptelement方法插入script节点时,尽量作为首个子节点插入到head中,这是由于一个难以发现的bug:
GLOBALEVALWORKSINCORRECTLYINIE6IFTHECURRENTPAGEHAS<BASEHREF>TAGINTHEHEAD
fetch模块
初始化Module对象时,状态为0,该对象对应的js文件并未加载,若要加载js文件,需要使用上节提到的request方法,但是也不可能仅仅加载该文件,还需要设置module对象的状态及其加载module依赖的其他模块。
这些逻辑在fetch方法中得以体现:
//Fetchamodule //加载该模块,fetch函数中调用了seajs.request函数 Module.prototype.fetch=function(requestCache){ varmod=this varuri=mod.uri mod.status=STATUS.FETCHING //Emit`fetch`eventforpluginssuchascomboplugin varemitData={uri:uri} emit("fetch",emitData) varrequestUri=emitData.requestUri||uri //Emptyurioranon-CMDmodule if(!requestUri||fetchedList[requestUri]){ mod.load() return } if(fetchingList[requestUri]){ callbackList[requestUri].push(mod) return } fetchingList[requestUri]=true callbackList[requestUri]=[mod] //Emit`request`eventforpluginssuchastextplugin emit("request",emitData={ uri:uri, requestUri:requestUri, onRequest:onRequest, charset:data.charset }) if(!emitData.requested){ requestCache? requestCache[emitData.requestUri]=sendRequest: sendRequest() } functionsendRequest(){ seajs.request(emitData.requestUri,emitData.onRequest,emitData.charset) } //回调函数 functiononRequest(){ deletefetchingList[requestUri] fetchedList[requestUri]=true //Savemetadataofanonymousmodule if(anonymousMeta){ Module.save(uri,anonymousMeta) anonymousMeta=null } //Callcallbacks varm,mods=callbackList[requestUri] deletecallbackList[requestUri] while((m=mods.shift()))m.load() } }
其中seajs.request就是上节的request方法。onRequest作为回调函数,作用是加载该模块的其他依赖模块。
总结
以上就是seajs模块的依赖加载及模块API的导出的全部内容了,小编会在下一节,将介绍模块之间依赖的加载以及模块的执行。感兴趣的朋友们可以继续关注毛票票。