宝哥软件园

Seajs源代码的详细分析

编辑:宝哥软件园 来源:互联网 时间:2021-08-26

近年来,前端工程越来越完善,封装工具已经是前端的标配。像seajs这样的老古董已经停止维护了,估计有几个用户。但这不能阻止我好奇。为了了解前端前辈是如何在浏览器中模块化代码的,我鼓起勇气打开了Seajs的源代码。让我们和我一起品味Seajs的源代码。

如何使用seajs

在阅读Seajs的源代码之前,我们先来看看Seajs是如何使用的。毕竟我们刚进入这个行业的时候,大家都用的是browserify、webpack之类的,从来没有用过Seajs。

!-首先在页面中引入sea.js,或者使用CDN资源-脚本类型=' text/JavaScript ' src=' http :/sea . js '/script script//要设置一些参数sea js . config({ debug 3360 true,//当debug为false时,脚本标记为base: '。模块加载后,head中的/js/'将被移除。//通过路径加载其他模块的默认根目录,别名alias: {//别名jquery : ' https://cdn.bootcss.com/jquery/3.2.1/jquery'}})seajs . use(' main ',参数配置函数(main){ alert(main)})/script//main . jsdefine(函数(require,exports,module){//require(' jquery ')//var $=window。$ module . exports=' main-module ' })seajs

首先,我们通过脚本导入seajs,然后对seajs进行一些配置。seajs的很多配置参数没有详细介绍。seajs将配置项存储在私有对象、数据中,如果之前设置了某个属性,并且该属性是数组或对象,则新值将与旧值合并。

(函数(全局,未定义){ if(global . seajs){ return } var data=seajs . data={ } seajs . config=function(config data){ for(config data中的var key){ var curr=config data[key]//获取当前配置var prev=data[key] //获取之前的配置if (prev isObject(prev)) {//如果之前已经设置过,并且对于(curr中的var k)的对象{ prev[k]=curr[k]//用覆盖旧值concat(curr)}//确保base是一个路径else if(key==' base '){//必须以if (curr.slice(-1)结尾!=='/'){ curr='/' } curr=addbase(curr)//转换为绝对路径}//设置config data[key]=curr } } } })(this);设置的时候有一个特殊的地方,那就是属性库。这代表所有模块加载的基本路径,所以格式必须是路径,路径最终会转换为绝对路径。例如,我的配置是base :’。/js ',我目前访问的域名是http://qq.com/web/index.html,最后基础属性会转换成http://qq.com/web/js/.然后,所有的依赖模块id都会按照路径转换成URIs,除非定义了其他配置,所以直接到配置使用的点。

模块的加载和执行

接下来,我们调用use方法,这是加载模块的地方,类似于requirejs中的require方法。

//requires requires([' main '],function(main){ console . log(main)});这里只有依赖项,seajs可以传入字符串,而requirejs必须是数组,seajs会把字符串转换成数组,seajs.use会直接在内部调用Module.use。这个module是一个构造函数,其中装载了所有与module加载相关的方法,还有很多静态方法,比如实例化Module、将module id转换为uri、定义Module等等。

Seajs.use=function (ids,callback) {module.use (ids,callback,data . CWD ' _ use _ ' cid())return seajs }//此方法用于加载一个匿名Module.use=function (ids,callback,uri) {//如果是通过Seajs.use调用,则uri自动生成var mod=module.get (uri,ISARRAY (IDS)?Ids : [ids] //这里,依赖模块将被转换成一个数组。)mod。_entry.push(mod) //表示当前模块的入口就是本身。稍后,该值将被传递到他的依赖模块mod . history={ } mod . resist=1//。稍后,该值将用于标识是否已经加载了所有相关模块。mod.callback=function() {//模块加载后设置回调。这部分很重要。尤其是exec方法varexports=[]varuri s=mod . resolve()for(var I=0,len=uris.length我透镜;I){导出[I]=缓存的MODS [URIs [I]。exec ()} if(回调){回调。apply (global,exports)//execute callback } } mod . load()}这个use方法总共做了三件事:

1.调用模块。获取实例化模块。2.绑定模块的回调函数。3.调用加载到依赖于加载的模块

实例化模块,一切的开始

首先,use方法调用get static方法,该方法实例化Module,将实例化的对象存储在全局对象cachedMods中进行缓存,并将uri作为模块标识符,如果以后有其他模块加载,可以直接从缓存中获取。

var cachedMods=seajs.cache={} //模块的缓存对象模块。get=function (uri,deps){返回缓存的MODS [uri] | |(缓存的MODS [uri]=newmodule (uri,deps绑定的回调函数))} functionmodule (uri,deps) {this。uri=uri this。deps=deps | |[]this。deps={ }//引用依赖模块。状态=0这。_ entry=[]}将在加载所有模块后调用。我们先跳过它,load方法首先将所有依赖的模块id转换成URIs,然后实例化,最后调用fetch方法,绑定模块加载成功或失败的回调,最后加载模块。具体代码如下(代码简化):

//加载完所有依赖项后,执行onload module . prototype . load=function(){ var mod=this mod . status=status.loading//set将状态设置为模块加载//调用resolve方法将模块id转换为uri。//比如前面的‘main’会加上我们之前设置的base前缀,然后用js后缀//拼写,最后变成:‘http://qq.com/web/js/main.js' varuris=mod . resolve()//遍历所有依赖项的uris,然后实例化依赖模块For(var I . I len;I){ mod . deps[mod . dependencies[I]]=module . get(URIs[I])}//将条目传递给所有依赖模块。条目是mod。传递()if (mod。_ entry.length) {mod。onload()return }//这是我们在使用方法时设置的,开始并行加载var request cache={ } var m for(I=0;我透镜;I){ m=cachedMods[uri[I]]//获取在//fetch { 0 }之前实例化的模块对象m . fetch(requestcache)//发送加载模块的请求(request cache中的var请求uri){ if(request cache . hasown property(request uri)){ request cache[request uri]()//调用seajs.request} }}将模块id转换为uri

解析方法的实现可以稍微看一下,它基本上是取出配置中的参数并处理拼接后的URIs。

module . prototype . resolve=function(){ var mod=this var ids=mod.dependencies//take取出所有依赖模块的ids varuri=[]//并对(var i=0,len=ids.length我透镜;I) {URIs [I]=模块。解析(id[I],mod。uri)//将模块id转换为uri}返回URIs}模块。resolve=function (id,ref ori){ var emit data={ id : id,ref ori : ref ori } return seajs . resolve(emit data . id,ref ori)//调用id2 uri } seajs . resolve=id2 uri function id2 uri(id,ref ori){//将id转换为uri并转换配置中的一些变量if(!id)return ' ' Id=parse alias(Id)Id=parse path(Id)Id=parse alias(Id)Id=parse vars(Id)Id=parse alias(Id)Id=normalize(Id)Id=parse alias(Id)varuri=add base(Id,ref ori)uri=parse alias(uri)uri=parse map(uri)return uri }最后,调用id2Uri将Id转换为uri,其中调用了许多解析方法。这些方法不一一考察,但原理大致相同。如果已经为这个id定义了别名,就把它取出来,比如id是‘jQuery’,而jQuery 3360‘https://CDN.bootcss.com/jQuery/3.2.1/jQuery'之前已经在别名的定义中定义过,那么这个id就会转换成‘https://CDN.bootcss.com/jQuery/3.码如下:

函数parseAlias(id) {//如果定义了Alias,用alias var alias=data对应的地址替换id。别名返回别名是字符串(别名[id])?别名[id] : id}为依赖项添加了一个条目,这便于追溯到源

解析后获取uri,通过uri实例化模块,然后调用传递方法。该方法主要记录入口模块有多少未加载的依赖项,将其存储在remain中,并将所有条目存储在依赖模块的_entry属性中,便于回溯。并且这个剩余用于计数,最后当onload的模块数等于剩余时,激活入口模块的回调。具体代码如下(代码简化):

module . prototype . pass=function(){ var mod=this var len=mod.dependencies.length//traverses entry模块的_entry属性,它一般只有一个值,也就是说,它可以回到use method-mod。_ entry . push(mod)for(var I=0;我修改了。_ entry.lengthI) {var entry=mod。_entry[i] //获取entry模块var count=0 //counter,用于对(var j=0;j lenJ) {var m=mod。国防部。dependencies[j]]//检索依赖模块//如果模块未加载且未在条目中使用,则将条目传递给依赖if (m.status STATUS。已加载!入口。history . hasown property(m . uri)){ entry . history[m . uri]=true//count m . entry . push(entry)//将entry模块存储到依赖模块的_entry属性中}}//如果卸载的依赖模块大于0 if (count 0) {//这里‘count-1’的原因也可以回过头来看看use method-mod。ret=1//ret的初始值为1,这意味着默认情况下会有一个未加载的模块。所有条目都需要减少1。remain=count-1//如果有未加载的依赖项,请删除条目模块的entry mode。_ entry.shift () I-}}}如何发起请求并下载其他依赖模块?

一般来说,pass方法会记录ret的值,然后是highlight,调用所有依赖项的fetch方法,然后加载依赖模块。调用fetch方法时,会传入一个requestCache对象,该对象用于缓存所有依赖模块的请求方法。

var请求缓存={ }为(I=0;我透镜;I){ m=cachedMods[uri[I]]//获取之前实例化的模块对象m.fetch(requestCache) //进行fetch }模块。原型。fetch=function(请求缓存){ var mod=this var uri=mod。uri mod。状态=状态.正在获取callbackList[Requesturi]=[mod]emit(' request ',emitData={ //设置加载脚本时的一些数据uri: uri,requestUri: requestUri,onRequest: onRequest,charset :是函数(数据。charset)?数据。charset(Requesturi):数据。charset,交叉原点:是FuncTion(数据。交叉起源)?数据。交叉原点(Requesturi):数据。交叉原点})if(!emitdata。必选){//发送请求加载射流研究…文件请求缓存[emitdata。请求uri]=发送请求}函数sendRequest() { //被请求方法,最终会调用seajs。请求seajs。请求(emitdata。请求uri,emitData.onRequest,emitData.charset,emitData.crossorigin) }函数onRequest(错误){ //模块加载完毕的回调var m,mods=callbackList[requestUri]删除callbackList[requestUri] //保存元数据到匿名模块,uri为请求射流研究…的uri if(anonymoussmeta){ module。save(uri,anonymoussmeta)anonymoussmeta=null }而((m=MODS。shift()){//发生404时参数错误将为如果(error===true){ m . error()} else { m . load()} }则为true经过取得操作后,能够得到一个requestCache对象,该对象缓存了模块的加载方法,从上面代码就能看到,该方法最后调用的是seajs.request方法,并且传入了一个请求回调。

for(请求缓存中的var请求uri){请求缓存[请求uri]()//调用seajs.request}//用来加载射流研究…脚本的方法seajs。请求=请求功能请求(URL,回调,字符集,交叉原点){ var node=doc。createelement(')脚本)addOnload(节点,回调,url) node.async=true //异步加载节点。src=URL头。appendchild(节点)}函数addOnload(节点、回调、URL){ node。onload=onload节点。onerror=function(){ emit(' error ',{ uri: url,node: node }) onload(true) }函数onload(错误){ node。onload=节点。onerror=节点。onreadystatechange=null/脚本加载完毕的回调回调(错误)}}通知入口模块

上面就是请求的逻辑,只不过删除了一些兼容代码,其实原理很简单,和模块化开发一样,都是创建脚本标签,绑定装载事件,然后插入头中。在装载事件发生时,会调用之前取得定义的请求方法,该方法最后会调用负荷方法。没错这个负荷方法又出现了,那么依赖模块调用和入口模块调用有什么区别呢,主要体现在下面代码中:

if (mod ._条目。长度){ mod。onload()return }如果这个依赖模块没有另外的依赖模块,那么他的进入就会存在,然后调用装载模块,但是如果这个代码中有规定方法,并且还有其他依赖项,就会走上面那么逻辑,遍历依赖项,转换uri,调用取得巴拉巴拉。这个后面再看,先看看装载会做什么。

模块。原型。onload=function(){ var mod=this mod。状态=状态.LOADED for (var i=0,len=(mod ._entry || []).长度;我透镜;i ) { var entry=mod ._条目[i] //每次加载完毕一个依赖模块,保持就-1 //直到留下为0,就表示所有依赖模块加载完毕if (- entry.remain===0) { //最后就会调用进入的回收方法//这就是前面为什么要给每个依赖模块存入entry.callback() } }删除mod ._条目}依赖模块执行,完成全部操作

还记得最开始使用方法中给入口模块设置回收方法吗,没错,兜兜转转我们又回到了起点。

Mod.callback=function() {//模块加载后设置回调。var exports=[]Varuris=mod . resolve()for(var I=0,len=uris.length我透镜;I) {//执行所有依赖模块的exec方法,并将其保存在exports数组exports [I]=cachedmods [URIs [I]]中。exec ()} if(回调){回调。应用(全局,导出)//执行回调}//删除一些属性deletemod。回调删除mod。历史删除模式。保持删除模式。_ entry}那么这位高管做了什么?

module . prototype . exec=function(){ var mod=this mod . STATUS=STATUS。正在执行if (mod。_条目!适度的_ entry.length) {delete mod。_ entry}函数require(id){ var m=mod . deps[id]return m . exec()} varfactory=mod.factory//call由define//定义的回调传入三个与commonjs相关的参数:require,module.exports,module var exports=factory . call(mod.exports={ },require,Mod . exports,Mod)if(exports===undefined){ exports=mod.exports//If该函数不返回值,只取Mod即可。exports} mod。导出=导出mod。状态=状态。executedreturn mod。导出//并返回模块的导出。}这里的工厂是在模块定义中定义的回调函数,例如,我们已经在main.js中定义了一个模块加载。

定义(函数(require,exports,module){ module . exports=' main-module ' })然后调用这个工厂时,导出就是module。出口,这也是串串的‘主模’。回调传入的最后一个参数是“main-moudle”。所以我们执行开头写的代码,最后我们会在页面上弹出main-moudle。

定义定义模块

你认为这就是结局吗?它没有。我刚才说过,在负载依赖模块的define方法中没有其他依赖项。如果有其他依赖关系呢?不用多说,让我们来看看define方法做了什么:

global . define=module . define emodule . define=function(ID,deps,The factory){ var argslen=arguments.length//parameter校准if(argslen===1){ factory=ID=undefined } else if(argslen===2){ factory=depsif(ISARRAY(ID)){ deps=ID=undefined } else { deps=undefined } }//如果依赖数组没有直接传入//,则将工厂中的所有依赖模块提取到dep数组if(!isArray(deps)is function(factory)){ deps=type of parseDependencies===' undefined '?[] :解析依赖项(factory . tostring())} varmeta={//模块加载和定义的元数据: id,uri : module.resolve (id),deps: deps,The factor : factory }//激活define事件,用于nocache插件、sea js节点版本等发出(' define ',meta) meta.uri?模块。保存(元。uri,meta)://加载脚本后保存匿名meta=meta。首先,修正参数。这个逻辑很简单,可以直接跳过。第二步,判断是否有依赖数组,如果没有,通过parseDependencies方法从工厂获取。这个方法很有趣。它是一个状态机,一步一步地解析字符串,根据需要匹配,取出其中的模块,最后放入数组。这种方法是通过requirejs中的规律性来实现的,seajs在早期也是通过规律性来匹配的。后来改成这个状态机,可能是性能问题。Seajs的仓库有专门的模块来讲这个东西,请看链接。

获取依赖模块后,设置一个元对象,表示这个模块的原始数据,记录模块的依赖、id、工厂等。如果这个模块没有设置id就定义了,说明是匿名模块,那么怎么能和之前发起请求的mod匹配呢?

有一个名为anonymousMeta的全局变量。首先,将元数据放入这个对象。然后,回头看看加载模块时的onload函数集,其中有一段是获取这个全局变量。

加载模块后,onRequest(错误){//回调函数.//将元数据保存到匿名模块,uri是if(匿名元){module。保存(uri,匿名元)匿名元=null}.}无论是否是匿名模块,元数据最终都是通过save方法存储在mod中。

//将元数据存储在cachedMods module . save=function(uri,meta){ var mod=module . get(uri)if(mod . STATUS STATUS)。SAVED){ Mod . id=meta . id | | uri Mod . dependencies=meta . deps | |[]Mod。factory=meta。factorymod。状态=状态。保存}}这里完成后,和前面的逻辑一样。首先,检查当前模块是否有依赖关系。如果有依赖,加载依赖的逻辑和使用的逻辑是一样的。加载完所有依赖项后,通知门户模块ret减1,知道ret为0,最后调用门户模块的回调方法。seajs的整个逻辑已经完全实现了,耶!

结论

看过requirejs的体验后,seajs仍然流畅得多,对模块化有了更深的理解。在阅读源代码之前,还是要对框架有一个基本的了解,并且使用过,否则很多地方都是无知的。所以,今后我们应该多阅读一些工作中经常用到的框架或者类库的源代码,而不是总是像无头苍蝇一样。

最后,用流程图总结了seajs的加载过程。

更多资讯
游戏推荐
更多+