第三课:sea.js模块加载原理

模块加载,其实就是把js分红不少个模块,便于开发和维护。所以加载不少js模块的时候,须要动态的加载,以便提升用户体验。node

在介绍模块加载库以前,先介绍一个方法。jquery

动态加载js方法:面试

function loadJs(url , callback){数组

  var node = document.createElement("script");浏览器

      node[window.addEventListener ? "onload":"onreadystatechange"] = function(){闭包

            if(window.addEventListener || /loaded|complete/i.test(node.readyState)){app

      callback();jsp

      node.onreadystatechange = null;模块化

    }                                                                             函数

  }

  node.onerror = function(){};

     node.src = url;

  var head = document.getElementsByTagName("head")[0];

  head.insertBefore(node,head.firstChild);     //插入到head的第一个节点前,防止ie6下head标签没闭合前,使用appendChild报错。 

}

 

我来说下sea.js的模块加载过程吧:

页面chaojidan.jsp,在head标签中,引入sea.js,这时就会获得seajs对象。

同时引入index.js。

index.js的代码以下:

seajs.use(['./a','jquery'],function(a,$){ var num = a.a; $('#J_A').text(num); })

a.js :

define(function(require,exports,module){ var b = require('./b'); var a = function(){ return 1 + parseInt(b.b()); } exports.a = a; })

b.js :

define(function(require,exports,module){ var c = require('./c'); var b = function(){ return 2 + parseInt(c.c()); } exports.b = b; })

c.js :

define(function(require,exports,module){ var c = function(){ return 3; } exports.c = c; })

由上可知,a模块依赖b,b依赖c.

当程序进入到index.js,seajs将调用use方法。


seajs.use = function(ids, callback) {
  globalModule._use(ids, callback)
}
说明: globalModule 为seajs初始化时(引入sea.js时),Module的实例 
var globalModule = new Module(util.pageUri, STATUS.COMPILED)

此时 ids -> ['./a','jquery'], callback -> function(a,$){var num = a.a;$('#J_A').text(num);}

 

接下来将调用 globalModule._use(ids, callback)

Module.prototype._use = function(ids, callback) {  
  var uris = resolve(ids, this.uri); //解析['./a','jquery'] this._load(uris, function() { //把解析出来的a,jquery模块的地址[url1,url2],调用_load方法。           //util.map : 让数据成员所有执行一次一个指定的函数,并返回一个新的数组,该数组为原数组成员执行回调后的结果 var args = util.map(uris, function(uri) { return uri ? cachedModules[uri]._compile() : null;//若是存在url,就调用_compile方法。
   })
   if (callback) { callback.apply(null, args) } 
  })

   }

由于调用_load方法后,会出现两个回调函数,所以咱们将function(a,$){var num = a.a;$('#J_A').text(num);}标志为callback1,
this._load(uris, function() { })回调方法标志为callback2.
resolve方法就是解析模块地址的,这里我就不细讲了。

最终var uris = resolve(ids, this.uri)中 的uris被解析成了['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js'],模块路径解析已经完毕。

而接下来将执行this._load

// _load()方法主要会先判断哪些资源文件尚未ready,若是所有资源文件都处于ready状态就执行callback2 // 在这其中还会作循环依赖的判断,以及对没有加载的js执行加载 Module.prototype._load = function(uris, callback2) {  
  //util.filter : 让数据成员所有执行一次一个指定的函数,并返回一个新的数组,该数组为原数组成员执行回调后返回为true的成员 //unLoadedUris是那些没有被编译的模块uri数组 var unLoadedUris = util.filter(uris, function(uri) { //返回执行函数布尔值为true的成员,在uri存在而且在内部变量cacheModules中不存在或者它在存储信息中status的值小于STATUS.READY时返回true // STATUS.READY值为4,小于四则可能的状况是获取中,下载中。 return uri && (!cachedModules[uri] || cachedModules[uri].status < STATUS.READY) });  
  //若是uris中的模块所有都ready好了,执行回调并退出函数体(这时就会调用模块的_compile方法了)。
  var length = unLoadedUris.length
  if (length === 0) { callback2() return }
  //还未加载的模块个数 var remain = length  //建立闭包,尝试去加载那些没有加载的模块 for (var i = 0; i < length; i++) { (function(uri) {  //判断若是在内部变量cachedModules里面并不存在该uri的存储信息则实例化一个Module对象 var module = cachedModules[uri] || (cachedModules[uri] = new Module(uri, STATUS.FETCHING))  //若是模块的状态值大于等于2,也就意味着模块已经被下载好并已经存在于本地了,这个时候执行onFetched() //不然则调用fetch(uri, onFetched) ,尝试下载资源文件,资源文件下载后会触发onload,onload中会执行回调onFetched的方法。 module.status >= STATUS.FETCHED ? onFetched() : fetch(uri, onFetched) function onFetched() { module = cachedModules[uri]  //当模块的状态值为大于等于STATUS.SAVED的时候,也就意味着该模块全部的依赖信息已经被拿到 if (module.status >= STATUS.SAVED) {  //getPureDependencies:获得不存在循环依赖的依赖数组 var deps = getPureDependencies(module)  //若是依赖数组不为空 if (deps.length) {  //再次执行_load()方法,直到所有依赖加载完成后执行回调 Module.prototype._load(deps, function() { cb(module) }) }  //若是依赖数组为空的状况下,直接执行cb(module) else { cb(module) } }  // 若是获取失败后,好比404或者不符合模块化规范 //在这种情形下,module.status会维持在 FETCHING 或者 FETCHED else { cb() } } })(unLoadedUris[i]) }  // cb 方法 - 加载完全部模块执行回调 function cb(module) {  // 若是module的存储信息存在,那么修改它的module存储信息中的status的值,修改成 STATUS.READY module && (module.status = STATUS.READY)  // 只有当全部模块加载完毕后执行回调。 --remain === 0 && callback2() } }
}

这里unLoadedUris的数组长度为2 ,['http://localhost/test/SEAJS/a.js','http://localhost/test/SEAJS/lib/juqery/1.7.2/juqery-debug.js'],因此 接下来会产生两个以 js路径为名称的闭包。

以http://localhost/test/SEAJS/a.js为例 
接下来 : 首先会建立一个Module:

cachedModules('http://localhost/test/SEAJS/a.js') = new Module('http://localhost/test/SEAJS/a.js',1)

module.status >= STATUS.FETCHED ? onFetched() : fetch(uri, onFetched)

由于此时a模块并无加载 因此接下来将会执行 fetch(uri, onFetched) 即fetch('http://localhost/test/SEAJS/a.js',onFetched)。

function fetch(uri, onFetched) {  // 根据map中的规则替换uri为新的请求地址 var requestUri = util.parseMap(uri)  // 首先在已获取列表中查找是否含有requestUri记录 if (fetchedList[requestUri]) {  // 这个时候将原始uri的module存储信息刷新到经过map重定义的requestUri上 cachedModules[uri] = cachedModules[requestUri]  // 执行onFetched 并返回,意味着模块已经获取成功了 onFetched() return } //在获取列表中查询 requestUri 的存储信息 if (fetchingList[requestUri]) {  //在callbacklist中加入该uri对应下的callback,并返回 callbackList[requestUri].push(onFetched) //若是正在获取中,就把此模块的onFetched回调方法push进数组中,并返回。 return }  // 若是尝试获取的模块都未出如今fetchedList和fetchingList中,则分别在请求列表和回调列表中添加其信息 fetchingList[requestUri] = true callbackList[requestUri] = [onFetched]  // Fetches it Module._fetch( requestUri, function() { fetchedList[requestUri] = true  // Updates module status // 若是 module.status 等于 STATUS.FECTCHING ,则修改module状态为FETCHED var module = cachedModules[uri] if (module.status === STATUS.FETCHING) { module.status = STATUS.FETCHED } if (fetchingList[requestUri]) { delete fetchingList[requestUri] }  // Calls callbackList 统一执行回调 if (callbackList[requestUri]) { util.forEach(callbackList[requestUri], function(fn) { fn()  //fn就是模块a对应的onFeched方法。 }) delete callbackList[requestUri] } }, config.charset ) }
接下来 将会执行 Module._fetch(),这里的回调函数咱们称做为callback3.

Module._fetch(),这里的回调函数咱们称做为callback3.

此方法就是调用loadJs方法动态下载a.js文件。(由于有a和jquery,因此会新建两个script),这里有一个疑问,新建a的script,并添加到head中,就会下载js文件,可是在seajs中,并无下载,而是等jquery的script创建好,并添加到head中,才会下载(谷歌调试器设断点,一直显示pending等待中)。这是为毛?
(推荐看这里:http://ux.sohu.com/topics/50972d9ae7de3e752e0081ff,这里我说下额外的问题,你们可能知道为何咱们要少用table来布局,由于table在呈现树布局的时候,须要屡次计算,而div只须要一次。同时,美的电商面试官告诉我:table须要所有解析完才会显示出来,而div解析多少就显示多少。经查证table中若是有tbody标签,就会按照tbody来分段显示。所以在IE6,7,8中,若是你用innerHTML来建立一个"<table></table>",会自动在里面添加<tbody></tbody>。)。
下载成功后,就会解析执行,执行的是define方法。这里会先执行a模块的代码。
define(id,deps,function(){})方法解析

此方法就是调用loadJs方法动态下载a.js文件。(由于有a和jquery,因此会新建两个script),这里有一个疑问,新建a的script,并添加到head中,就会下载js文件,可是在seajs中,并无下载,而是等jquery的script创建好,并添加到head中,才会下载(谷歌调试器设断点,一直显示pending等待中)。这是为毛?
(推荐看这里:http://ux.sohu.com/topics/50972d9ae7de3e752e0081ff,这里我说下额外的问题,你们可能知道为何咱们要少用table来布局,由于table在呈现树布局的时候,须要屡次计算,而div只须要一次。同时,美的电商面试官告诉我:table须要所有解析完才会显示出来,而div解析多少就显示多少。经查证table中若是有tbody标签,就会按照tbody来分段显示。所以在IE6,7,8中,若是你用innerHTML来建立一个"<table></table>",会自动在里面添加<tbody></tbody>。)。
下载成功后,就会解析执行,执行的是define方法。这里会先执行a模块的代码。
define(id,deps,function(){})方法解析

//define 定义 ,id : 模块id , deps : 模块依赖 , factory  Module._define = function(id, deps, factory) {

   //解析依赖关系 // 若是deps不是数组类型,同时factory是函数

   if (!util.isArray(deps) && util.isFunction(factory)) { // 函数体内正则匹配require字符串,并造成数组返回赋值给deps

     deps = util.parseDependencies(factory.toString())

   }

  //设置元信息

   var meta = { id: id, dependencies: deps, factory: factory } 

 

   if (document.attachEvent) {

     // 获得当前script的节点

     var script = util.getCurrentScript()

       // 若是script节点存在

     if (script) {

         // 获得原始uri地址

         derivedUri = util.unParseMap(util.getScriptAbsoluteSrc(script)) }

         if (!derivedUri) {

             util.log('Failed to derive URI from interactive script for:', factory.toString(), 'warn')

         }

     }

 .........

 

 }
}

  define首先会对factory执行一个判断 ,判断它是否为一个函数(缘由是由于define内也能够包括文件,对象)

 

若是是函数 , 那么 就会经过factory.toString(),获得函数,并经过正则匹配得 a.js的依赖,并把依赖保存在 deps 中

 

对于 a.js 而言, 它的依赖 是 b.js 因此 deps为 ['./b']

 

并对 a.js 的信息进行保存 var meta = { id: id, dependencies: deps, factory: factory }

 

针对a.js meta = { id : undefined , dependencies : ['./b'] , factory : function(xxx){xxx}}

 

在 ie 6-9 浏览器中能够拿到当前运行js的路径 可是在标准浏览器中 ,这不可行 ,因此暂时先把元信息赋值给anonymousModuleMeta = meta。

 

而后触发onload,这时就会调用回调方法callback3,此回调方法就会修改当前回调模块(a.js)的状态值,将其设置为 module.status = STATUS.FETCHED。

再接下来 ,将统一 执行回调队列 callbackList 中的 a.js所对应的回调,也就是onFetched

onFetched方法会检查a模块是否有依赖模块,由于a依赖于b,因此对模块a所依赖的b.js 执行_load()。

会去下载b模块,这时会先执行jquery的define方法。由于jquery没依赖模块,因此onload回调后。onFetched调用cb方法。

 

 

当b按照a同样的过程实现后,就会下载c模块。最终c,b,a模块都下载执行define,并onload结束后,也会调用cb方法,(先c,再b,后c)

全部模块都为ready以后,就会调用callback2方法。
最终回调到callback2,执行a和jquery模块的最终回调到callback2,执行a和jquery模块的_compile方法:

首先编译a.js模块,模块a的function执行,由于a里面有require(b.js),所以会去执行b模块的function.
模块 a 的function开始执行
模块 b 的function开始执行
模块 c 的function开始执行
模块 c 的function执行完毕
模块 b 的function执行完毕
模块 a 的function执行完毕

最后执行jquery的function。

编译结束后,就执行callback1,就可使用a和jquery对象了。

seajs版本已经更新,如今没有_compile方法了。(你们自行去看,我也要去看下)

加油!
相关文章
相关标签/搜索