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的导出的全部内容了,小编会在下一节,将介绍模块之间依赖的加载以及模块的执行。感兴趣的朋友们可以继续关注毛票票。