分类: Web开发
2013-01-15 16:29:19
前段时间我开始基于SeaJS开发2.0版本的jRaiser,主要目的就是把这个库模块化。结合实际开发过程中遇到的问题,我重新写了一个更符合自身需求的jRaiser Loader以代替SeaJS(另一方面也是为了亲手写一个Loader)。与SeaJS一样,jRaiser Loader也是Wrappings规范(关于AMD与Wrappings的区别,这篇文章有详细说明)的实现,主要接口也与SeaJS保持一致(但功能比SeaJS少)。下面以jRaiser Loader的实现为例介绍一下Loader的实现原理。
在模块化开发中,一个模块可以依赖于任意个模块,而被它依赖的模块又可以依赖于任意个其他模块。这就要求加载模块时必须一层一层把所有依赖的模块都加载进来,类似于树的遍历。由于前端动态加载JS的过程是异步的,也就是说,这是异步的遍历。在算法上,jRaiser Loader采取自顶向下的遍历方式以及自底向上的通知方式。假设有A、B、C三个模块,A依赖于B,B依赖于C。当通过jRaiser.use加载A模块时,其加载过程如下(红色箭头部分为异步流程):
jRaiser Loader的功能主要由四个类协作完成,其中最核心的是Module类。
Module对象有两种,一种表示功能接口模块,另一种表示需要执行的任务模块。前者通过define()创建,id为该模块的路径;后者通过jRaiser.use()创建,id为“#自动编号”。通过isTask方法可以知道该对象是否任务模块。
当加载模块定义文件时,该文件调用的define()就会创建Module对象。Module类的构造函数有三个参数:
为何作为模块标识的id属性可以为空呢?这主要是浏览器特征决定的。前面提到过,模块路径即为模块Id。在IE下,只要遍历script元素,并找出readyState属性为interactive的元素,就可以获取到当前正在执行的script,进而通过src属性获取到它的路径;但是其他浏览器下的script元素没有readyState属性,就只能借助onload事件获取刚刚执行完的script元素(这招在IE下偏偏又是无效的,因为IE的onload事件有可能不是紧接着script执行结束触发的),所以延迟了Id的设置。
Module对象的初始化工作大多要在设置了Id之后才能进行,所以放到了setId方法中。setId方法会检查每个依赖模块是否就绪,如果未就绪,则调用dependentChain.add()添加依赖记录,并调用Module.load()加载该模块,Module.load则调用scriptLoader.load加载外部JS文件,此为自顶向下的模块加载流程。由于scriptLoader内部记录了文件加载状态,所以同一个文件不会被重复加载多次。最后,setId方法会调用_checkReady方法检查当前模块是否就绪。
原文参考自
一旦某Module对象已经就绪,它会执行以下操作:
另外,notifyReady方法也会调用_checkReady方法检查当前Module对象是否就绪,如果就绪,则对当前对象再执行上面的操作。此为自底向上的就绪通知流程。
jRaiser Loader目前仅达到了Loader的最基本功能,一些异常的情况(例如循环依赖)尚未处理,有待进一步完善。jRaiser 2.0正式版在短期内还不会与大家见面,有兴趣的朋友可自行从SVN检出代码。
最后,我把一个未能很好解决的问题提出来,希望高手支招。很多JS类库提供了在DOMReady之后执行回调函数的功能,比如jQuery:
$(function() { alert('dom ready'); });
但是,如果把这个功能作为一个模块异步加载进来使用的话,可能会失效。原因在于,模块加载完成后,DOMReady这个时机可能已经过去。某个事件已经过去,你再给它绑回调函数,就只能在下次触发这个事件的时候才会执行,可惜的是DOMReady只会触发一次。同样的问题也存在于onload事件中。
我暂时把DOMReady回调绑定放在了Loader文件中,并且,为避免每个需要使用DOMReady接口的模块都直接依赖于Loader,又另外建立了一个domready模块调用Loader的接口(jRaiser.domReady)。如果将来有更优雅的实现,就可以只修改domready模块。