模块加载,其实就是把js分成很多个模块,便于开发和维护。因此加载很多js模块的时候,需要动态的加载,以便提高用户体验。

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

动态加载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)){

      callback();

      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.

此方法就是调用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模块的_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方法了。(大家自行去看,我也要去看下)

加油!


第三课:sea.js模块加载原理的更多相关文章

  1. sea.js模块加载工具

    seajs的使用 seajs是一个jS模块加载器,由淘宝前端架构师玉伯开发,它可以解决命名空间污染,文件依赖的问题.可以在一个js文件中引入另外一个js.require('a.js') 1.安装 np ...

  2. AMD-require.js模块加载原理

    项目中使用大了require.js,功能实现,现重新学习下模块加载原理相关知识,借鉴如下博文:https://blog.csdn.net/ai52011/article/details/7711361 ...

  3. js模块化/js模块加载器/js模块打包器

    之前对这几个概念一直记得很模糊,也无法用自己的语言表达出来,今天看了大神的文章,尝试根据自己的理解总结一下,算是一篇读后感. 大神的文章:http://www.css88.com/archives/7 ...

  4. js模块加载之AMD和CMD

    当我写这篇文章的时候,sea.js已经逐渐退出历史的舞台,详细链接.不过任何新事物的出现都是对旧事物的取其精华,去其糟粕,所以了解一下以前模块的加载也是一件好事. js模块化的原因自不比多说,看看HU ...

  5. Node.js require 模块加载原理 All In One

    Node.js require 模块加载原理 All In One require 加载模块,搜索路径 "use strict"; /** * * @author xgqfrms ...

  6. nodejs js模块加载

    本文地址:http://www.cnblogs.com/jasonxuli/p/4381747.html nodejs的非核心模块(core module)加载主要使用的就是module.js. 项目 ...

  7. 实现简单的 JS 模块加载器

    实现简单的 JS 模块加载器 1. 背景介绍 按需加载是前端性能优化的一个重要手段,按需加载的本质是从远程服务器加载一段JS代码(这里主要讨论JS,CSS或者其他资源大同小异),该JS代码就是一个模块 ...

  8. js模块加载框架 sea.js学习笔记

    seajs实现了JavaScript 的 模块开发及按模块加载.用来解决繁琐的js命名冲突,文件依赖等问题,其主要目的是令JavaScript开发模块化并可以轻松愉悦进行加载. 官方文档:http:/ ...

  9. 【 js 模块加载 】深入学习模块化加载(node.js 模块源码)

    一.模块规范 说到模块化加载,就不得先说一说模块规范.模块规范是用来约束每个模块,让其必须按照一定的格式编写.AMD,CMD,CommonJS 是目前最常用的三种模块化书写规范.  1.AMD(Asy ...

随机推荐

  1. [系统开发] 基于Ansible的产品上线系统

    前言: 应部门急需,开发了一套基于Ansible Playbook的产品上线系统.由于时间很紧,UI直接套用了之前开发的一套perl cgi模板,后续计划用 django 重新编写. 个人感觉该系统的 ...

  2. 关于request.getParameterMap()的类型转换和数据获取

    首先po上一个自己写的转换类. /** * @author Xfiler * @described 将request.getParameterMap()转换为普通的Map的工具方法 * @param ...

  3. Python torndoa mysql 模块安装

    pip install torndb pip install pip install mysql-python #不支持3.x版本 ln -s /usr/local/mysql/lib/libmysq ...

  4. 在shiro-cas中实现 Jasig-cas的Single Sign Out 功能

    1 Single Sign Out 功能 即单点登出功能.也就是在任意子系统进行登出操作后,其他子系统会自动登出. 实际CAS登出的步骤为 所以每个子系统都需要实现一个sso登出响应. cas-cli ...

  5. Retina Display and Eclipse Mac视网膜屏和Eclipse

    I have a Retina Display and Eclipse looks blurry. How can I fix it? You need to tell Mac OS that Ecl ...

  6. iOS 8潜在的取证问题

    Apple于今天正式发布了iOS 8推送升级 大概琢磨了一下: 1. 可以确定,iOS 7中存在的File relay等所谓后门服务已经被修正,目前Oxygen和我们采用这种服务提取的功能将不再适用于 ...

  7. UNIX网络编程-非阻塞connect和非阻塞accept

    1.非阻塞connect 在看了很多资料之后,我自己的理解是:在socket发起一次连接的时候,这个过程需要一段时间来将三次握手的过程走完,如果在网络状况不好或者是其他的一些情况下,这个过程需要比较长 ...

  8. vmware 修改IP 提示子网掩码错误~

    我打开[编辑]->[虚拟网络编辑器]菜单 修改网络设置 提示 子网掩码错误 如下图所示 上网查询,使用如下方法解决问题 这个虚拟网络编辑器是给你添加网卡的,你添加vmnet1就是在你真实的电脑上 ...

  9. c#与js中10进制16进制的转化,记录防忘

    js: var param="11"; param=parseInt(param,16);  //17 param=parseInt(param,10);  //11 //后面的参 ...

  10. wc命令

    wc命令的功能为统计指定文件中的字节数.字数.行数, 并将统计结果显示输出. 语法:wc [选项] 文件- 说明:该命令统计给定文件中的字节数.字数.行数.如果没有给出文件名,则从标准输入读取.wc同 ...