You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Module.prototype.load=function(){varmod=this// If the module is being loaded, just wait it onload callif(mod.status>=STATUS.LOADING){return}mod.status=STATUS.LOADING//拿到解析后的依赖模块的列表varuris=mod.resolve()//复制_remainvarlen=mod._remain=uris.lengthvarmfor(vari=0;i<len;i++){//拿到依赖的模块对应的实例m=Module.get(uris[i])if(m.status<STATUS.LOADED){// Maybe duplicate: When module has dupliate dependency, it should be it's count, not 1//把我注入到依赖的模块里的_waitings,这边可能依赖多次,也就是在define里面多次调用require加载了同一个模块。所以要递增m._waitings[mod.uri]=(m._waitings[mod.uri]||0)+1}else{mod._remain--}}//如果一开始就发现自己没有依赖模块,或者依赖的模块早就加载好了,就直接调用自己的onloadif(mod._remain===0){mod.onload()return}//检查依赖的模块,如果有还没加载的就调用他们的fetch让他们开始加载for(i=0;i<len;i++){m=cachedMods[uris[i]]if(m.status<STATUS.FETCHING){m.fetch()}elseif(m.status===STATUS.SAVED){m.load()}}}Module.prototype.onload=function(){varmod=thismod.status=STATUS.LOADED//回调,预留接口给之后主函数use使用,这边先不管if(mod.callback){mod.callback()}varwaitings=mod._waitingsvaruri,m//遍历依赖自己的那些模块实例,挨个的检查_remain,如果更新后为0,就帮忙调用对应的onloadfor(uriinwaitings){if(waitings.hasOwnProperty(uri)){m=cachedMods[uri]m._remain-=waitings[uri]if(m._remain===0){m.onload()}}}}
javascript模块加载器实践
但凡是比较成熟的服务端语言,都会有模块或者包的概念。模块化开发的好处就不用多说了。由于javascript的运行环境(浏览器)的特殊性。js很早之前一直都没有模块的概念。经过一代代程序猿们的努力。提供了若干的解决方案。
基本对象
为了解决模块化的问题。早期的程序员会把代码放到某个变量里。做一个最简单的命名空间的划分。
比如一个工具模块:util
这样所有的工具函数都托管在util这个对象变量里,极其简陋的弄了个伪命名空间。这样的局限性很大,因为我们可以随意修改。util不存在私有的属性。_prefix这个私有属性,后面可以随意修改。而我们很难定位到到底在哪边被修改了。
闭包立即执行
后来,一些程序员想到了方法解决私有属性的问题,有了下面这种写法:
主要使用了匿名函数立即执行的技巧,这样
_prefix
是一个匿名函数里面的局部变量,外面无法修改。但是log这个函数里面又因为闭包的关系可以访问到_prefix。只把公用的方法暴露出去。这是后来模块划分的主要技巧,各大库比如jQuery,都会在最外层包裹这样一个匿名函数。
但是这只是在同一个文件里面的技巧,如果我们把util单独写到一个文件util.js。而我们程序的主代码是main.js那我们需要在页面里面一起用script标签引入:
这会有不少问题,最典型的比如如果我们的main.js如下:
这个就执行不了,因为我们的util.js是在main.js后面引入的。所以执行main.js的内容的时候util还没定义呢。
不止这个问题,再比如如果引入了其他的js文件,并且也定义了util这个变量。就会混乱。
模块加载器
node作为javascript服务端的一种应用场景,加入了文件模块的概念,主要是实现的CommonJS规范。
后来一些程序员就想,服务端可以有文件模块。浏览器端为什么就不可以呢。但是CommonJS规范是设计给服务端语言用的,不适合浏览器端的js。
于是出现了amd规范,并且在这个基础上出现了实现amd规范的库requirejs。
后来国内的大神玉伯由于多次给requirejs提建议(比如用时定义)一直不被采纳。于是另起炉灶制作了seajs。慢慢的也沉淀出了seajs的cmd规范。
关于模块规范的具体历史,可以参考:seajs/seajs#588
两个规范差别并不是很大,可能由于写node习惯了,个人更喜欢cmd的编写方式。
首先我们看看基于cmd规范(其实就是seajs)后我们怎么写代码:
seajs的书写风格跟node很像。
seajs会自动帮你加载好模块的文件,并且正确的处理依赖关系。于是前端终于也可以使用模块化的开发方式了。
一步一步实现模块加载器
下面我们来实现一个简单的cmd模块加载器程序,也可以当作是seajs的核心源码分析。
获取加载根路径
cmd模块规定一个模块一个文件,当我们
require('util')
的时候需要找到对应的文件,一般会加上根路径。默认情况下加载模块的根路径就是seajs.js所在目录。如何获取这个目录地址呢?我们只要在seajs.js里面写上:这边有两个小技巧:
scripts[scripts.length - 1]
拿到引用seajs.js的那个script节点引用。getAttribute("src", 4)
的方式获取绝对地址。参考这里。ie67下没有 hasAttribute属性,所以就有了获取绝对地址的兼容写法。异步js文件加载器
模块加载是建立在文件加载器基础上的。在浏览器环境下我们可以通过动态生成script标记的方式,加载js。我们写一个简单js文件加载器:
主要就是动态生成一个script节点加载js,监听事件触发回调函数,没什么难度,算是一个工具函数,给下面的模块使用。
模块类定义
终于到了重头戏。我们需要引入一个模块类的概念。util,main这些都是一个模块。模块有自己的依赖,有自己的状态。
我们先定义一个模块类:
1.uri代表当前模块的地址,一般是使用baseUrl(就是上面的loadderDir)+ id + '.js'
2.dependencies是当前模块依赖的模块。
3.factory就是我们定义模块时define的参数
function(require, exports, module){}
4.status代表当前模块的状态,我们先定义下面这些状态:
5.
_waitings
存放着依赖我的模块实例集合,_remain
则代表我还有多少依赖模块是处于不可用,也就是上面的小于LOADED的状态。这个的作用是什么呢?
是这样的,比如A模块依赖B,C模块。那么A模块装载的时候会先去通知B,C模块把自己(A)加入到他们的
_waitings
里面。当B模块装载好了,就可以通过遍历B自己的_waitings
去更新依赖它的模块比如A的_remain
值。B发现更新后A的_remain
后不为0,就什么也不做。直到C也好了,C更新下A的_remain
值发现为0了,就会调用A的完成回调了。如果B,C有自己的依赖模块也是一样的原理。
而如果一个模块没有依赖的模块,就会立即进入完成状态,然后通知依赖它的模块更新
_remain
值。他们处于最底端,往上一级级的去更新状态。模块相互之间的通知机制就是这样,那么状态是如何变化的呢。
我们给模块增加一些原型方法:
是不是感觉有点晕,没事我们一个个来看。
辅助函数
我们先把辅助函数实现下:
fetch与define的实现
实现fetch之前我们先实现全局函数define。
fetch会生成script节点加载模块的具体代码。
还记得我们上面模块定义的写法吗?都是使用define来定义一个模块。define的主要任务就是生成当前模块的一些信息,给fetch使用。
define的实现:
这边为了尽量展现原理,去掉了很多兼容的代码。
比如其实define是支持
function (id, deps, factory)
这种写法的,这样就可以提前写好模块的id和deps,这样就不需要通过正则去获取依赖的模块了。一般写的时候只写factory,上线时会使用构建工具生成好deps参数,这样可以避免压缩工具把require关键字压缩掉而导致依赖失效。性能上也会更好。另外,为了兼容ie下面的script标签不一定触发的问题。这边其实有个getCurrentScript()的方法,用于获取当前正在解析的script节点的地址。这边略去,有兴趣的可以去源码里看看。
下面是fetch的实现:
load与onload的实现
fetch完成后会调用load方法。
我们看下load的实现:
这样整个通知机制就结束了。
exec的实现
模块onload之后代表已经处于一种可执行状态。seajs不会立即执行模块代码,只有你真正require了才会去调用模块的exec去执行。这就是用时定义。
入口函数seajs.use
上面这套东西已经完成了整个模块之间的加载执行依赖关系了。但是还缺少一个入口。
这时候就是seajs.use出场的时候了。seajs.use用来加载一些模块。比如下面:
其实我们可以把它当作一个主模块,use的后面那些比如main就是它的依赖模块。而且这个主模块比较特殊,他不需要经过加载的过程,直接可以从load装载开始,于是use的实现就很简单了:
于是整个流程就变成了这样:
主入口函数use直接生成一个模块,直接load。然后建立好依赖关系。通过上面那套通知机制,从下到上一个个的触发模块的onload。然后主函数里面调用依赖模块的exec去执行,然后一层层的下去,每一层都可以通过require来执行对应的factory。整个过程就是这样。
结语
又是一个因为js本身的缺陷,然后后人擦屁股的事情。这样的例子已经数不胜数了。js真是让人又爱又恨。总之有了模块加载器,让js有了做大规模富客户端应用的能力。是前端工业化开发不可缺少的一环。
The text was updated successfully, but these errors were encountered: