你的位置:首页 > Java教程

[Java教程]requireJs的模块加载和依赖机制的分析和简单实现。


requireJs的文件加载和依赖管理确实非常好用,相信大家都有这个体会。在此之前,我们的html文件头部总是要有一长串的script标签来引入js文件,并且还必须非常注意script标签的先后顺序。 

这篇文章对requireJs的核心功能做了简单实现,希望能帮助大家更好理解requireJs.

下面的思路是我参考了requireJs 0.0.7版本实现的。之前有尝试理解当前版本的requireJs的源码,不过最后发现,这特么不是短时间能搞的定的。 无奈之下找了github上先前较早的版本,那时还没有那么多配置项,代码结构更简单一点。

--------------------boom----------------

首先,假设我们有这样一个文件结构 

js/require.js

js/main.js

js/a.js  js/b.js  js/a1.js  js/a2.js js/b1.js   js/b2.js

index.html

我们的入口文件时main.js, 在入口文件中,我们调用了require函数

require(["a","b"],function(a,b){  // do something.});

我们看到上面的require函数中,回掉函数的执行依赖于a和b两个模块

然后我们的a.js文件像这样

define("a",["a1","a2"],function(a1,b1){  //do something});

可以看到a模块依赖于a1,a2模块。

a1模块像这样

define("a1",function(){  //do something});

 

同理b模块依赖于b1,b2模块,文件结构类似。

 

------------------boom--------------

 

先说说require和define函数的关系。 

require和define函数接收同样的参数,不同的是,define函数被建议在一个文件中使用一次,用它来定义模块。

require函数一般在入口文件或者顶层代码中使用,用来加载和使用模块。

其实在我看来,require函数可以看做是特殊的define函数,它用来定义一个顶层匿名模块,这个模块不需要被其他模块加载。

二者的区别这里有一些介绍 requirejs中define和require的定位以及使用区别?

 

requireJs中的执行流程

一,requrieJs首先找到data-main属性,然后根据属性值(通过新建一个script标签)加载并且解析入口文件。

  下面看入口文件中的 require(["a","b"],function(){})调用发生了什么?

二,在require函数中,我们先生成一个简单的模块对象,大概是这样的

{moduleName:"_@$1",deps:["a","b"],callback:function(){},callbackReturn:null,args:[]}

对这个模块对象属性的解释:

moduleName:模块名称。 前面我们说了,require函数可以看做是定义一个匿名的顶层模块对象。所以这里生成了一个内部名称"_@$1"

deps: 依赖数组; 包含当前模块依赖的模块。

callback:回调函数。 require中的那个回调函数。

callbackReturn:回调函数返回值    (其实貌似这里并不需要这个属性,我主要考虑到用这个属性来存储模块回调函数的返回值,这样当我们多次依赖这个模块时,可以直接返回这个值。)

args:数组,对应于依赖模块的传递回来的值

 

我们在全局设置一个context对象

  

context = {};  context.topModule = ""; //存储requre函数调用生成的顶层模块对象名。  context.modules = {}; //存储所有的模块。使用模块名作为key,模块对象作为value  context.loaded = []; //加载好的模块  (加载好是指模块所在的文件加载成功)  context.waiting = [];//等待加载完成的模块

 

我们在这里设置

    context.topModule = "_@$1";   //因为当前定义的是一个顶层匿名模块,所以生成一个内部模块名。

    context.modules 中添加_@$1模块,结果像这样

 {"_@$1":{moduleName:"_@$1",deps:["a","b"],callback:function(){},callbackReturn:null,args:[]}}

 

 

    context.waiting 中添加依赖的模块,结果像这样 ["a","b"];    //把依赖的模块添加到waiting中 (其实这里还可以优化为先判断依赖模块是否已经存在于context.loaded中)

然后我们遍历依赖数组 ["a","b"],分别创建script标签并加载,绑定好data-moduleName属性,和加载完成回调函数onscriptLoaded ,在遍历中大概像这样

  

var script = document.createElement("script");  script.onload = onscriptLoaded; //脚本加载好后的回调函数。 这是个核心函数  script.setAttribute("data-moduleName","a"); //为script元素添加data-moduleName属性,方便在回调函数中判断当前模块  script.src = "js/a.js";  document.getElementsByTagName("head")[0].appendChild(script);

 

到这里require函数就完成了。

三。假设上面的js/a.js加载好了,文件中执行了

define("a",["a1","a2"],function(a1,b1){  //do something});
我们看看define中做了哪些事。其实define函数和上面的require函数做了差不多相同的事,差别在于require自动生成了一个模块名。并且require中设置了context.topModule.

生成模块 {moduleName:"a",deps:["a1","a2"],callback:function(){},callbackReturn:null,args:[]}
修改全局context变量
  context.modules中添加当前模块 ,结果如下
{
"_@$1":{moduleName:"_@$1",deps:["a","b"],callback:function(){},callbackReturn:null,args:[]},
"a":{moduleName:"a",deps:["a1","a2"],callback:function(){},callbackReturn:null,args:[]}}

  context.waiting添加当前依赖数组。  --结果   ["a","b","a1","b1"]

然后接着根据依赖数组创建script标签,绑定data-moduleName属性,绑定回调函数onscriptLoaded

 

四。最后的关键函数onscriptLoaded

function onscriptLoaded(event){

  思路大概是这样。

  1.根据event对象我们可以得到加载完成的script元素,得到它的data-moduleName属性,这个属性就是模块名

  2.在全局context对象中,给 context.loaded数组中加上这个模块名。 context.waiting数组中减去这个模块名。

  3.接下来判断, 如果context.waiting数组不为空则返回。 

  4.否则如果context.waiting为空数组,表明所有的依赖都已经加载了。

  接下来就是重头戏。

  5.创建一个递归函数来执行模块回调函数,像这样

  

function exec(module) {    var deps = module.deps;  //当前模块的依赖数组  var args = module.args;  //当前模块的回调函数参数  for (var i = 0, len = deps.length; i < len; i++) { //遍历     var dep = context.modules[deps[i]];                 args[i] = exec(dep); //递归得到依赖模块返回值作为对应参数  }  return module.callback.apply(module, args); // 调用回调函数,传递给依赖模块对应的参数。}var topModule = context.modules[context.topModule]; //找到顶层模块。exec(topModule); //开始执行递归函数

}  //onscriptLoaded结束

整个实现的思路就是,我们在define和require中定义模块时,所有的依赖的模块名都被添加到了context.waiting数组中。 每个依赖在加载时的script标签都绑定了onload事件,在事件回调函数中我们把当前模块名从context.waiting中删除,接着我们判断context.waiting是否为空,为空时意味着所有模块的文件都加载好了,此时就可以从顶层模块开始,使用一个递归函数来执行模块的回调函数。

 

最后

  我本来就只是想写出一个核心的思路,所以代码中很多地方还值得琢磨,可能并不正确,但整体的思路没错。

  注意这里我在使用define函数时,模块名参数我并没有省略,这是因为,在本片文章的实现思路中,我并没有更多的篇幅来解释怎么来实现define函数的省略模块名。 大概的思路可能是在define执行时,我们并不知道当前定义的模块的模块名,所以我们创建一个临时的模块名,然后全局中设置一个变量temp指向这个模块。 考虑到define函数执行完后,它所在的script标签的onload事件必然会紧接着触发,而且这个script标签上有data-moduleName绑定了正确的模块名,所以我们可以在onload事件回调函数中找到temp指向的模块,然后修改它的模块名。

 之前本来准备写篇关于requireJs api的详解,最后发现自己墨水有限,好多东西只可意会不能言传,最后放弃了。   如果关于这篇文章大家有什么好的意见和建议,请与我讨论,我们一起来完善这篇文章。