Seajs源码详解分析
近几年前端工程化越来越完善,打包工具也已经是前端标配了,像seajs这种老古董早已停止维护,而且使用的人估计也几个了。但这并不能阻止好奇的我,为了了解当年的前端前辈们是如何在浏览器进行代码模块化的,我鼓起勇气翻开了seajs的源码。下面就和我一起细细品味seajs源码吧。
如何使用seajs
在看seajs源码之前,先看看seajs是如何使用的,毕竟刚入行的时候,大家就都使用browserify、webpack之类的东西了,还从来没有用过seajs。
seajs的参数配置
首先通过script导入seajs,然后对seajs进行一些配置。seajs的配置参数很多具体不详细介绍,seajs将配置项会存入一个私有对象data中,并且如果之前有设置过某个属性,并且这个属性是数组或者对象,会将新值与旧值进行合并。
设置的时候还有个比较特殊的地方,就是base这个属性。这表示所有模块加载的基础路径,所以格式必须为一个路径,并且该路径最后会转换为绝对路径。比如,我的配置为base: './js'
,我当前访问的域名为http://qq.com/web/index.html
,最后base属性会被转化为http://qq.com/web/js/
。然后,所有依赖的模块id都会根据该路径转换为uri,除非有定义其他配置,关于配置点到为止,到用到的地方再来细说。
模块的加载与执行
下面我们调用了use方法,该方法就是用来加载模块的地方,类似与requirejs中的require方法。
只是这里的依赖项,seajs可以传入字符串,而requirejs必须为一个数组,seajs会将字符串转为数组,在内部seajs.use会直接调用module.use。这个module为一个构造函数,里面挂载了所有与模块加载相关的方法,还有很多静态方法,比如实例化module、转换模块id为uri、定义模块等等,废话不多说直接看代码。
这个use方法一共做了三件事:
1.调用module.get,进行module实例化
2.为模块绑定回调函数
3.调用load,进行依赖模块的加载
实例化模块,一切的开端
首先use方法调用了get静态方法,这个方法是对module进行实例化,并且将实例化的对象存入到全局对象cachedmods中进行缓存,并且以uri作为模块的标识,如果之后有其他模块加载该模块就能直接在缓存中获取。
绑定的回调函数会在所有模块加载完毕之后调用,我们先跳过,直接看load方法。load方法会先把所有依赖的模块id转为uri,然后进行实例化,最后调用fetch方法,绑定模块加载成功或失败的回调,最后进行模块加载。具体代码如下(代码经过精简)
:
将模块id转为uri
resolve方法实现可以稍微看下,基本上是把config里面的参数拿出来,进行拼接uri的处理。
最后就是调用了id2uri
,将id转为uri,其中调用了很多的parse
方法,这些方法不一一去看,原理大致一样,主要看下parsealias
。如果这个id有定义过alias,将alias取出,比如id为"jquery"
,之前在定义alias中又有定义jquery: 'https://cdn.bootcss.com/jquery/3.2.1/jquery'
,则将id转化为'https://cdn.bootcss.com/jquery/3.2.1/jquery'
。代码如下:
为依赖添加入口,方便追根溯源
resolve之后获得uri,通过uri进行module的实例化,然后调用pass方法,这个方法主要是记录入口模块到底有多少个未加载的依赖项,存入到remain中,并将entry都存入到依赖模块的_entry属性中,方便回溯。而这个remain用于计数,最后onload的模块数与remain相等就激活entry模块的回调。具体代码如下(代码经过精简)
:
如何发起请求,下载其他依赖模块?
总的来说pass方法就是记录了remain的数值,接下来就是重头戏了,调用所有依赖项的fetch方法,然后进行依赖模块的加载。调用fetch方法的时候会传入一个requestcache对象,该对象用来缓存所有依赖模块的request方法。
经过fetch操作后,能够得到一个requestcache
对象,该对象缓存了模块的加载方法,从上面代码就能看到,该方法最后调用的是seajs.request
方法,并且传入了一个onrequest回调。
通知入口模块
上面就是request的逻辑,只不过删除了一些兼容代码,其实原理很简单,和requirejs一样,都是创建script标签,绑定onload事件,然后插入head中。在onload事件发生时,会调用之前fetch定义的onrequest方法,该方法最后会调用load方法。没错这个load方法又出现了,那么依赖模块调用和入口模块调用有什么区别呢,主要体现在下面代码中:
如果这个依赖模块没有另外的依赖模块,那么他的entry就会存在,然后调用onload模块,但是如果这个代码中有define
方法,并且还有其他依赖项,就会走上面那么逻辑,遍历依赖项,转换uri,调用fetch巴拉巴拉。这个后面再看,先看看onload会做什么。
依赖模块执行,完成全部操作
还记得最开始use方法中给入口模块设置callback方法吗,没错,兜兜转转我们又回到了起点。
那么这个exec到底做了什么呢?
这里的factory就是依赖模块define中定义的回调函数,例如我们加载的main.js
中,定义了一个模块。
那么调用这个factory的时候,exports就为module.exports,也是是字符串"main-moudle"
。最后callback传入的参数就是"main-moudle"
。所以我们执行最开头写的那段代码,最后会在页面上弹出main-moudle
。
define定义模块
你以为到这里就结束了吗?并没有。前面只说了加载依赖模块中define方法中没有其他依赖,那如果有其他依赖呢?废话不多说,先看看define方法做了什么:
首先进行了参数的修正,这个逻辑很简单,直接跳过。第二步判断了有没有依赖数组,如果没有,就通过parsedependencies方法从factory中获取。这个方法很有意思,是一个状态机,会一步步的去解析字符串,匹配到require,将其中的模块取出,最后放到一个数组里。这个方法在requirejs中是通过正则实现的,早期seajs也是通过正则匹配的,后来改成了这种状态机的方式,可能是考虑到性能的问题。seajs的仓库中专门有一个模块来讲这个东西的,请看链接。
获取到依赖模块之后又设置了一个meta对象,这个就表示这个模块的原数据,里面有记录模块的依赖项、id、factory等。如果这个模块define的时候没有设置id,就表示是个匿名模块,那怎么才能与之前发起请求的那个mod相匹配呢?
这里就有了一个全局变量anonymousmeta
,先将元数据放入这个对象。然后回过头看看模块加载时设置的onload函数里面有一段就是获取这个全局变量的。
不管是不是匿名模块,最后都是通过save方法,将元数据存入到mod中。
这里完成之后,就是和前面的逻辑一样了,先去校验当前模块有没有依赖项,如果有依赖项,就去加载依赖项和use的逻辑是一样的,等依赖项全部加载完毕后,通知入口模块的remain减1,知道remain为0,最后调用入口模块的回调方法。整个seajs的逻辑就已经全部走通,yeah!
结语
有过看requirejs的经验,再来看seajs还是顺畅很多,对模块化的理解有了更加深刻的理解。阅读源码之前还是得对框架有个基本认识,并且有使用过,要不然很多地方都很懵懂。所以以后还是阅读一些工作中有经常使用的框架或类库的源码进行阅读,不能总像个无头苍蝇一样。
最后用一张流程图,总结下seajs的加载过程。
推荐阅读