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:"[email protected]$1",deps:["a","b"],callback:function(){},callbackReturn:null,args:[]}

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

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

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

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

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

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

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

  

context = {};

  context.topModule = "";  //存储requre函数调用生成的顶层模块对象名。

  context.modules = {}; //存储所有的模块。使用模块名作为key,模块对象作为value

  context.loaded = []; //加载好的模块   (加载好是指模块所在的文件加载成功)

  context.waiting = [];//等待加载完成的模块

我们在这里设置

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

    context.modules 中添加[email protected]$1模块,结果像这样

 {
"[email protected]$1":{moduleName:"[email protected]$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中添加当前模块  ,结果如下
{"[email protected]$1":{moduleName:"[email protected]$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的详解,最后发现自己墨水有限,好多东西只可意会不能言传,最后放弃了。   如果关于这篇文章大家有什么好的意见和建议,请与我讨论,我们一起来完善这篇文章。

时间: 2024-08-26 00:29:36

requireJs的模块加载和依赖机制的分析和简单实现。的相关文章

如何解决requireJs的模块加载超时

requireJs的加载是一种异步机制,它加载js的时候有个默认的超时机制,因为网络原因或者资源找不到等原因引起的 可以修改配置参数waitSeconds (默认为7秒,可以设置为0表示永远不超时,或者大一点的数字) require.config({ paths: { "jquery": "jquery-1.9.min" }, waitSeconds: 15 });

seaJS 模块加载过程分析

先看一个seajs的官方example,  以下以seajs.use('main')为例, 解析加载mod main的过程 //app.html seajs.use("main"); //main.js define(function(require) { var Spinning = require('./spinning'); var s = new Spinning('#container'); s.render(); }); //spinning.js define(funct

JavaScript模块化:使用requireJS按需加载

模块加载器的概念可能稍微接触过前端开发的童鞋都不会陌生,通过模块加载器可以有效的解决这些问题: JS文件的依赖关系. 通过异步加载优化script标签引起的阻塞问题 可以简单的以文件为单位将功能模块化并实现复用 主流的JS模块加载器有requireJS,SeaJS等,加载器之间可能会因为遵循的规范不同有微妙的差别,从纯用户的角度出发,之所以选requireJS而不是SeaJS主要是因为: 功能实现上两者相差无几,没有明显的性能差异或重大问题. 文档丰富程度上,requireJS远远好于SeaJS

js与AMD模块加载

目的: 了解AMD规范与CMD规范,写一个模块加载器雏形. 基本概念: AMD是异步模块定义规范,而CMD是通用模块定义规范.其他的还有CommonJS Modules规范. 对于具体的规范,可以参考: https://github.com/amdjs/amdjs-api/wiki/AMD AMD规范 https://github.com/seajs/seajs/issues/242 CMD规范 http://www.zhihu.com/question/20351507/answer/1485

第三章:模块加载系统(requirejs)

任何一门语言在大规模应用阶段,必然要经历拆分模块的过程.便于维护与团队协作,与java走的最近的dojo率先引入加载器,早期的加载器都是同步的,使用document.write与同步Ajax请求实现.后来dojo开始以JSONP的方法设计它的每个模块结构.以script节点为主体加载它的模块.这个就是目前主流的加载器方式. 不得不提的是,dojo的加载器与AMD规范的发明者都是james Burke,dojo加载器独立出来就是著名的require.本章将深入的理解加载器的原理. 1.AMD规范

Angular Material串串学客户端开发 2 - Node.js模块加载机制Require()

题外话解一下博客标题,因为第一篇文章评论中,有人质疑离题很远,说了半天和Angular Material没有半毛关系.其实我的的中心在后半句<串串学客户端开发>. require() 不要把这里的Require()和RequireJS混为一谈.不过有意思的是,Typescript的模块定义,甚至同时支持这两种模块机制. 导入和使用外部模块,只是简单的一句require(),看看angular/material/docs下的编译文件gulpfile.js的代码片段.对模块导入和使用有个直观的感觉

模块加载机制与包的加载机制

模块加载机制 包是更好的组织功能的代码结构 当前指定一个文件夹,会将文件夹内的index.js作为程序的入口    3 如果当前目录下有一个和文件夹同名的文件,优先加载文件        1 如果当前目录下有一个和文件夹同名的json文件,优先加载json  2 对于文件的加载,顺序是,如果你写上./xxx,优先会加载xxx.js/xxx.json/xxx.node 加载优先级,例如: ./abc 1:先检查当前目录下./abc目录中的package.json文件的main属性,满足就立刻加载

Java类加载机制——如何实现一个工程中不同模块加载不同版本的同名jar包。

如何实现一个工程中不同模块加载不同版本的同名jar包? 曾是阿里面试的时候遇到的一个面试题.当时就有点晕,如果是平时遇到这样的问题肯定是统一加载相同版本的就好了. 而如果系统特别庞大依赖冲突繁多,涉及多部门协调,真的有必要解决这样的问题. 这个问题困扰我很久,一直没有好的解决办法.最近研究java虚拟机.终于找到了答案. 在JVM里由类名和类加载器区别不同的Java类型.因此,JVM允许我们使用不同的加载器加载相同namespace的java类,而实际上这些相同namespace的java类可以

深入浅出nodejs(一) 模块加载机制

声明: 深入浅出nodejs系列文章将会在后面持续更新. 该系列文章部分参考 朴灵<深入浅出nodejs>,并加以总结补充 你真的了解require函数吗? 看似简单的require函数, 其实内部做了大量工作...下面我们将详细说明require为我们所做的一切 nodejs模块加载原理 node加载模块步骤: 1) 路径分析 (如判断是不是核心模块.是绝对路径还是相对路径等) 2) 文件定位 (文件扩展名分析, 目录和包处理等细节) 3) 编译执行 原生模块加载顺序 1) 缓存 2) 本地