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 数组循环引起的思考
Jan 01 Javascript
限制文本框输入N个字符的js代码
May 13 Javascript
新增加的内容是如何将div的scrollbar自动移动最下面
Jan 02 Javascript
jquery常用操作小结
Jul 21 Javascript
jQuery使用append在html元素后同时添加多项内容的方法
Mar 26 Javascript
jQuery添加和删除输入文本框标签代码
May 20 Javascript
JS产生随机数的几个用法详解
Jun 22 Javascript
jQuery弹出层后禁用底部滚动条(移动端关闭回到原位置)
Aug 29 Javascript
关于javascript作用域的常见面试题分享
Jun 18 Javascript
详解vue-admin和后端(flask)分离结合的例子
Feb 12 Javascript
layer弹出子iframe层父子页面传值的实现方法
Nov 22 Javascript
Vue.extend 编程式插入组件的实现
Nov 18 Javascript
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 分页原理分析,大家可以看看
2009/12/21 PHP
基于php-fpm的配置详解
2013/06/03 PHP
浅析51个PHP处理字符串的函数
2013/08/02 PHP
yii权限控制的方法(三种方法)
2015/12/28 PHP
php5.6.x到php7.0.x特性小结
2019/08/17 PHP
图解Sublime Text3使用技巧
2015/12/21 Javascript
利用jquery制作滚动到指定位置触发动画
2016/03/26 Javascript
javascript中错误使用var造成undefined
2016/03/31 Javascript
Bootstrap每天必学之标签页(Tab)插件
2020/08/09 Javascript
js定义类的几种方法(推荐)
2016/06/08 Javascript
很棒的js Tab选项卡切换效果
2016/08/30 Javascript
AngularJS的ng-click传参的方法
2017/06/19 Javascript
vue鼠标移入添加class样式,鼠标移出去除样式(active)实现方法
2018/08/22 Javascript
vue-i18n结合Element-ui的配置方法
2019/05/20 Javascript
VUE 组件转换为微信小程序组件的方法
2019/11/06 Javascript
基于JQuery和DWR实现异步数据传递
2020/10/16 jQuery
Python中不同进制互相转换(二进制、八进制、十进制和十六进制)
2015/04/05 Python
Python随机生成带特殊字符的密码
2016/03/02 Python
打包发布Python模块的方法详解
2016/09/18 Python
Python编程实现微信企业号文本消息推送功能示例
2017/08/21 Python
Python探索之爬取电商售卖信息代码示例
2017/10/27 Python
详解安装mitmproxy以及遇到的坑和简单用法
2019/01/21 Python
python字典setdefault方法和get方法使用实例
2019/12/25 Python
python中pdb模块实例用法
2021/01/15 Python
英国领先的NHS批准的在线药店:Pharmacy2U
2017/01/06 全球购物
Ariat官网:美国马靴和服装品牌
2019/12/16 全球购物
编写一个类体现构造,公有,私有方法,静态,私有变量
2013/08/10 面试题
小学后勤管理制度
2014/01/14 职场文书
工作求职自荐信
2014/06/13 职场文书
本科生自荐信
2014/06/18 职场文书
北京申奥口号
2014/06/19 职场文书
励志演讲稿600字
2014/08/21 职场文书
地道战观后感2000字
2015/06/04 职场文书
小人国观后感
2015/06/11 职场文书
zabbix自定义监控nginx状态实现过程
2021/11/01 Servers
MYSQL优化之数据表碎片整理详解
2022/04/03 MySQL