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字符串对象toLowerCase方法入门实例(用于把字母转换为小写)
Oct 17 Javascript
node.js中的emitter.emit方法使用说明
Dec 10 Javascript
JS实现的文字与图片定时切换效果代码
Oct 06 Javascript
JS+CSS实现闪烁字体效果代码
Apr 05 Javascript
js 打开新页面在屏幕中间的实现方法
Nov 02 Javascript
实例详解JavaScript中setTimeout函数的执行顺序
Jul 12 Javascript
vue2.0 常用的 UI 库实例讲解
Dec 12 Javascript
关于JavaScript中高阶函数的魅力详解
Sep 07 Javascript
Mpvue中使用Vant Weapp组件库的方法步骤
May 16 Javascript
20个必会的JavaScript面试题(小结)
Jul 02 Javascript
layer.open组件获取弹出层页面变量、函数的实例
Sep 25 Javascript
微信小程序实现轨迹回放的示例代码
Dec 13 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内置的字符串处理函数详解
2017/02/04 PHP
基于php中echo用逗号和用点号的区别详解
2018/01/23 PHP
PHP实现动态创建XML文档的方法
2018/03/30 PHP
PDO::inTransaction讲解
2019/01/28 PHP
Jquey拖拽控件Draggable使用方法(asp.net环境)
2010/09/28 Javascript
jQuery 在光标定位的地方插入文字的插件
2012/05/10 Javascript
jQuery插件ImageDrawer.js实现动态绘制图片动画(附源码下载)
2016/02/25 Javascript
举例说明JavaScript中的实例对象与原型对象
2016/03/11 Javascript
值得分享的轻量级Bootstrap Table表格插件
2016/05/30 Javascript
JS如何设置cookie有效期为当天24点并弹出欢迎登陆界面
2016/08/04 Javascript
jQuery实现的分页功能示例
2017/01/22 Javascript
JavaScript常用截取字符串的三种方式用法区别实例解析
2018/05/15 Javascript
JS实现头条新闻的经典轮播图效果示例
2019/01/30 Javascript
为nuxt项目写一个面包屑cli工具实现自动生成页面与面包屑配置
2019/09/29 Javascript
浅析vue-router中params和query的区别
2019/12/24 Javascript
node.js中 mysql 增删改查操作及async,await处理实例分析
2020/02/11 Javascript
详解vue中v-on事件监听指令的基本用法
2020/07/22 Javascript
js获取图片的base64编码并压缩
2020/12/05 Javascript
Python Mysql自动备份脚本
2008/07/14 Python
基于Linux系统中python matplotlib画图的中文显示问题的解决方法
2017/06/15 Python
Python列表list内建函数用法实例分析【insert、remove、index、pop等】
2017/07/24 Python
详解Python的hasattr() getattr() setattr() 函数使用方法
2018/07/09 Python
详解PyCharm安装MicroPython插件的教程
2019/06/24 Python
对Pytorch中nn.ModuleList 和 nn.Sequential详解
2019/08/18 Python
美国知名女性服饰品牌:New York & Company
2017/03/23 全球购物
Ellesse英国官网:意大利高级运动品牌
2019/07/23 全球购物
美团网旗下网上订餐平台:美团外卖
2020/03/05 全球购物
Java中会存在内存泄漏吗,请简单描述
2016/12/22 面试题
十月份红领巾广播稿
2014/01/22 职场文书
素食餐饮项目创业计划书
2014/02/02 职场文书
财务会计大学生自我评价
2014/04/09 职场文书
群众路线教育实践活动自我剖析思想汇报
2014/10/04 职场文书
指导老师鉴定意见
2015/06/05 职场文书
沂蒙六姐妹观后感
2015/06/08 职场文书
2015年音乐教研组工作总结
2015/07/22 职场文书
用Python selenium实现淘宝抢单机器人
2021/06/18 Python