javascript框架设计读书笔记之模块加载系统


Posted in Javascript onDecember 02, 2014

模块加载,其实就是把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报错。 

}

由于司徒正美使用了它写的mass框架来介绍模块加载,而业界用的最多的是require.js和sea.js。因此,我觉得他个性有点强。

我来讲下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对象了。

PS:seajs版本已经更新,现在没有_compile方法了。(大家自行去看,我也要去看下)

接着讲下seajs的模块编译_compile过程。

首先是a.js的编译

Module.prototype._compile = function() {

126     var module = this          

127     // 如果该模块已经编译过,则直接返回module.exports

128     if (module.status === STATUS.COMPILED) {

129       return module.exports

130     }

133     //  1. the module file is 404.

134     //  2. the module file is not written with valid module format.

135     //  3. other error cases.

136     // 这里是处理一些异常情况,此时直接返回null

137     if (module.status < STATUS.SAVED && !hasModifiers(module)) {

138       return null

139     }

140     // 更改模块状态为COMPILING,表示模块正在编译

141     module.status = STATUS.COMPILING

142 

143     // 模块内部使用,是一个方法,用来获取其他模块提供(称之为子模块)的接口,同步操作

144     function require(id) {

145       // 根据id解析模块的路径

146       
var uri = resolve(id, module.uri)

147       
// 从模块缓存中获取模块(注意,其实这里子模块作为主模块的依赖项是已经被下载下来的)

148       
var child = cachedModules[uri]

149 

150       
// Just return null when uri is invalid.

151       
// 如果child为空,只能表示参数填写出错导致uri不正确,那么直接返回null

152       
if (!child) {

153         
return null

154       
}

155 

156       
// Avoids circular calls.

157       
// 如果子模块的状态为STATUS.COMPILING,直接返回child.exports,避免因为循环依赖反复编译模块

158       
if (child.status === STATUS.COMPILING) {

159         
return child.exports

160       
}

161       
// 指向初始化时调用当前模块的模块。根据该属性,可以得到模块初始化时的Call Stack.

162       
child.parent = module

163       
// 返回编译过的child的module.exports

164       
return child._compile()

165     }

166     // 模块内部使用,用来异步加载模块,并在加载完成后执行指定回调。

167     require.async = function(ids, callback) {

168       module._use(ids, callback)

169     }

170     // 使用模块系统内部的路径解析机制来解析并返回模块路径。该函数不会加载模块,只返回解析后的绝对路径。

171     require.resolve = function(id) {

172       return resolve(id, module.uri)

173     }

174     // 通过该属性,可以查看到模块系统加载过的所有模块。

175     // 在某些情况下,如果需要重新加载某个模块,可以得到该模块的 uri, 然后通过 delete require.cache[uri] 来将其信息删除掉。这样下次




使用时,就会重新获取。

176     require.cache = cachedModules

177 

178     // require是一个方法,用来获取其他模块提供的接口。

179     module.require = require

180     // exports是一个对象,用来向外提供模块接口。

181     module.exports = {}

182     var factory = module.factory

183 

184     // factory 为函数时,表示模块的构造方法。执行该方法,可以得到模块向外提供的接口。

185     if (util.isFunction(factory)) {

186       compileStack.push(module)

187       runInModuleContext(factory, module)

188       compileStack.pop()

189     }

190     // factory 为对象、字符串等非函数类型时,表示模块的接口就是该对象、字符串等值。

191     // 如:define({ "foo": "bar" });

192     // 如:define('I am a template. My name is {{name}}.');

193     else if (factory !== undefined) {

194       module.exports = factory

195     }

196 

197     // 更改模块状态为COMPILED,表示模块已编译

198     module.status = STATUS.COMPILED

199     // 执行模块接口修改,通过seajs.modify()

200     execModifiers(module)

201     return module.exports

202   }
if (util.isFunction(factory)) {

186       compileStack.push(module)

187       runInModuleContext(factory, module)

188       compileStack.pop()

189     }

这里就是把module.export进行初始化。runInModuleContext方法:

// 根据模块上下文执行模块代码

489   function runInModuleContext(fn, module) {

490     // 传入与模块相关的两个参数以及模块自身

491     // exports用来暴露接口

492     // require用来获取依赖模块(同步)(编译)

493     var ret = fn(module.require, module.exports, module)

494     // 支持返回值暴露接口形式,如:

495     // return {

496     //   fn1 : xx

497     //   ,fn2 : xx

498     //   ...

499     // }

500     if (ret !== undefined) {

501       module.exports = ret

502     }

503   }

执行a.js中的function方法,这时会调用var b = require("b.js"),
require方法会返回b的compile方法的返回值,b模块中又有var c = require('c.js')。
这时会调用c的compile方法,然后调用c的function,c中,如果要暴露对象,或者是return 对象c,则模块c的exports = c。或者直接是module.export = c;总之最后会返回module c.export = c;所以var c = module c.export = c,模块b中,就可以使用变量c调用模块c中的c对象的方法和属性。
以此类推,最终a模块也能调用b模块中b对象的属性和方法。
不管什么模块,只要使用了module.export = xx模块,其他模块就可以使用require("xx模块"),调用xx模块中的各种方法了。
最终模块的状态会变成module.status = STATUS.COMPILED。

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) } 

})

   }

这时args = [module a.export, module jquery.export];

seajs.use(['./a','jquery'],function(a,$){

    var num = a.a;

    $('#J_A').text(num);

})

这时function中的a和$就是module a.export和module jquery.export。

因为本人现在在研究jquery源码和jquery框架设计,因此共享一些经验:
jquery源码,我在网上看了很多解析,看着看着就看不下去了。意义不大,推荐妙味课堂的jquery源码解析。

司徒正美的javascript框架设计,个人觉得难度大,但是精读后,你就是高级前端工程师了。

玉伯的sea.js,我建议去学习,去用,毕竟是中国人自己做的。我们公司新的项目或者重构,都会使用seajs来做。

接下来就是模块化handbars以及mvc的backbone或者mvvm的angular的源码精读。这里我希望有人给我提建议,看什么书,看什么网站,看什么视频能够快速的学习。

Javascript 相关文章推荐
JavaScript中各种编码解码函数的区别和注意事项
Aug 19 Javascript
用jquery实现自定义风格的滑动条实现代码
Apr 26 Javascript
jQuery.holdReady()使用方法
May 20 Javascript
Javascript实现简单的富文本编辑器附演示
Jun 16 Javascript
jquery+json实现动态商品内容展示的方法
Jan 14 Javascript
js实现表格筛选功能
Jan 18 Javascript
vue.js模仿京东省市区三级联动的选择组件实例代码
Nov 22 Javascript
详解react-redux插件入门
Apr 19 Javascript
JS算法题之查找数字在数组中的索引位置
May 15 Javascript
Vue 打包体积优化方案小结
May 20 Javascript
详解datagrid使用方法(重要)
Nov 06 Javascript
jquery实现点击左右按钮切换图片
Jan 27 jQuery
javascript框架设计读书笔记之种子模块
Dec 02 #Javascript
推荐一个封装好的getElementsByClassName方法
Dec 02 #Javascript
CSS3,HTML5和jQuery搜索框集锦
Dec 02 #Javascript
JavaScript和CSS交互的方法汇总
Dec 02 #Javascript
HTML,CSS,JavaScript速查表推荐
Dec 02 #Javascript
javascript函数声明和函数表达式区别分析
Dec 02 #Javascript
javascript常用方法汇总
Dec 02 #Javascript
You might like
PHP 存储文本换行实现方法
2010/01/05 PHP
PHP is_dir() 判断给定文件名是否是一个目录
2010/05/10 PHP
php5 apache 2.2 webservice 创建与配置(java)
2011/01/27 PHP
关于php程序报date()警告的处理(date_default_timezone_set)
2013/10/22 PHP
php通过修改header强制图片下载的方法
2015/03/24 PHP
php实现json编码的方法
2015/07/30 PHP
ecshop添加菜单及权限分配问题
2017/11/21 PHP
PHP 面向对象程序设计之类属性与类常量实现方法分析
2020/04/13 PHP
了解一点js的Eval函数
2012/07/26 Javascript
Jquery利用mouseenter和mouseleave实现鼠标经过弹出层且可以点击
2014/02/12 Javascript
JavaScript头像上传插件源码分享
2016/03/29 Javascript
ie下js不执行的几种可能
2017/02/28 Javascript
javascript 中Cookie读、写与删除操作
2017/03/29 Javascript
JavaScript基于面向对象实现的猜拳游戏
2018/01/03 Javascript
浅谈webpack4.x 入门(一篇足矣)
2018/09/05 Javascript
对Layer UI 模块化的用法详解
2019/09/26 Javascript
基于JS实现计算24点算法代码实例解析
2020/07/23 Javascript
[01:40]2014DOTA2国际邀请赛 三冰SOLO赛后采访恶搞
2014/07/09 DOTA
[56:21]LGD vs IG 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/19 DOTA
python实现电子词典
2020/04/23 Python
简述Python中的面向对象编程的概念
2015/04/27 Python
python3.0 模拟用户登录,三次错误锁定的实例
2017/11/02 Python
Python xlwt设置excel单元格字体及格式
2020/04/18 Python
Python列表推导式与生成器表达式用法示例
2018/02/08 Python
Python 字符串处理特殊空格\xc2\xa0\t\n Non-breaking space
2020/02/23 Python
python爬虫利器之requests库的用法(超全面的爬取网页案例)
2020/12/17 Python
Numpy中的数组搜索中np.where方法详细介绍
2021/01/08 Python
Stefania Mode英国:奢华设计师和时尚服装
2017/10/23 全球购物
如何用Python来进行查询和替换一个文本字符串
2014/01/02 面试题
写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数
2016/04/13 面试题
酒店办公室文员岗位职责
2013/12/18 职场文书
廉洁自律承诺书2015
2015/01/22 职场文书
2015年政务公开工作总结
2015/05/19 职场文书
MySQL配置主从服务器(一主多从)
2021/08/07 MySQL
动画《朋友游戏》公开佐藤友生绘制的开播纪念绘
2022/04/06 日漫
springcloud整合seata
2022/05/20 Java/Android